Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Archive

Where retired entries go.

docs/release-runbook.md says never to delete a feature entry: either strike its heading through and set status: retired, or move the whole entry here and leave a one-line stub in the feature list pointing at it. Heavy refactors move the old text here too.

This file is append-only and unordered. It is not a changelog and nothing reads it in sequence; it exists so that a question about why something was removed has an answer that is still in the tree, rather than only in a squashed commit message.

An entry keeps the key block it had when it was live, plus the reason it was retired and what replaced it.

It is empty because nothing has been retired yet. The runbook named it before it existed, which is the documentation form of a capability declared and not delivered; TestEveryDocumentationLinkResolves in internal/defaults is what noticed, and what will notice the next one.

The binary

Binary

mcpx ships as one Go binary, built from cmd/mcpx. This page states what is shipped — what the binary contains, what it needs next to it, and how the two packaged forms are built — not what could be built. Where a fact changes per release channel, this page says so and stops; release and install channels are a different page’s subject.

What is in it

The version is 0.1.0, hard-coded as the default in cmd/mcpx/main.go and stamped by the package with -X main.version=0.1.0; a build may override it with the same -ldflags variable, and nothing else about the binary changes.

Everything else is the module’s direct dependencies. go.mod is the source of truth — seven direct requirements:

modulewhat it is for
modernc.org/sqlitethe log index: a pure-Go translation of SQLite, not a cgo binding
github.com/charmbracelet/bubbleteathe full-screen browser, mcpx tui
github.com/charmbracelet/bubblesits widgets
github.com/charmbracelet/lipglossits layout
gopkg.in/yaml.v3OpenAPI documents, which are published as YAML as often as JSON
github.com/santhosh-tekuri/jsonschema/v6JSON Schema validation (internal/schemaval)
github.com/tetratelabs/wazerothe WebAssembly runtime behind wasm tool sources and embeddings (internal/wasmtools)

The three charmbracelet modules are one decision: about 1.2 MB in the stripped binary, in exchange for not hand-writing terminal handling. dependencies.md argues each of the original five in full; it and package.nix still say “five” because both predate the last two rows — trust go.mod, and this table, over that count.

Two absences are as deliberate as the presences:

  • No MCP SDK. mcpx is built on none — there is no MCP module in go.mod. The MCP client (internal/mcpclient), the server (internal/mcpserver) and the JSON-RPC framing are mcpx’s own code, about a package each. That is most of why the dependency list stays this short.
  • Everything else is the standard library: the daemon and its HTTP API, the process pools, the JSON Schema to TypeScript compiler, the CLI.

Every direct dependency is pure Go, so a CGO_ENABLED=0 build is fully static, and that is what the Nix package ships: env.CGO_ENABLED = "0" in package.nix, stripped (-s -w), so the shipped binary is a static executable that needs no libc next to it.

The container image makes the other choice, on purpose for now: its Containerfile defaults CGO_ENABLED to 1, so the binary it ships links glibc and the runtime stage is Debian bookworm-slim. A static container build was measured working end-to-end before that default was chosen — see container.md, which this page links rather than restates, because the container is still changing.

The behavioural difference between the two is narrow and worth stating exactly: with cgo off, pure-Go os/user reads /etc/passwd rather than asking NSS/LDAP, and the pure-Go resolver bypasses NSS modules — on Linux only; on macOS, CGO_ENABLED=0 changes neither, because Go uses the system resolver and user database there without cgo either way. Neither difference prevents a static build, and this page never claims they do; which form to ship where is a policy question, tracked with the release work, not a technical block.

Size, as last measured in container.md: the mcpx binary is about 14.8 MB in the container build. (The 21 MB figure sometimes quoted is an older linux/arm64 static build; measure the artifact you are shipping, not either number here.)

What it needs next to it

The binary alone answers ls, types, catalog, search and call. The rest of what it reaches for is external programs — the full table, with what degrades when each is missing, is in dependencies.md:

  • A JavaScript runtime — deno, bun or node — for mcpx run and mcpx exec only. Without one, those two commands fail and everything else still works.
  • git — a fallback only. mcpx reads .git itself for the repo and worktree scopes (see git-discovery.md) and runs git rev-parse only for a repository layout its own reader does not vouch for. Without git, those rare layouts degrade to keying by directory, and mcpx doctor says why.
  • The configured MCP servers themselves — spawned with the command, args, env and cwd the config names, or reached over Streamable HTTP for url servers.

The Nix package wraps the binary with a PATH suffix naming git, deno, bun-bin and nodejs, so those four are findable without depending on the calling shell — and because it is a suffix, a runtime already on your PATH wins over the wrapped one. The same package installs the generated man page and shell completions (see the CLI page).

The container image ships exactly one runtime — bun — and sets MCPX_SCRIPT_RUNTIME=bun, so the runtime choice inside the image is unambiguous; it leaves git out entirely, for the reason above. What the image deliberately does not contain, and why, is container.md’s subject.

The CLI

The mcpx command line, in three parts: how a session with it normally goes, the two kinds of flag it accepts, and where the full command reference lives. The CLI is one of the client interfaces — everything it does goes through the daemon’s /v1 API, and anything a person can do here, a script or a plugin can do there.

The CLI

The loop

Almost every session is the same four steps (the same loop AGENTS.md teaches agents):

  1. mcpx ls — which namespaces exist. Cheap; run it whenever.
  2. mcpx search <words> — find a tool by name or description.
  3. mcpx types <namespace> — load signatures for that namespace only (or mcpx types <namespace>.<tool> for one tool, which is far cheaper). mcpx catalog fits every namespace into a token budget when you are still deciding.
  4. mcpx run <name> or mcpx exec '<code>' — do the work.

Do not run mcpx types for every namespace: loading one costs a few hundred tokens, loading all of them costs thousands and defeats the point of keeping schemas out of context until asked.

Output a program can read

Human output is for humans. Anything that has to be parsed gets --json instead:

mcpx --json ls
mcpx --json run screenshot

--json returns one document — for a script run, the result, the logs, the exit code and the duration. Never parse the human tables.

Bare invocation, and a runnable first argument

Bare mcpx opens the TUI (the tui browser) when there is a terminal to draw on, and prints help when there is not — a pipe gets the old behaviour.

A first argument that is obviously a script runs without naming a command: a path ending in .ts, .js, .mts or .mjs, any existing file, or text that is obviously source (it contains (){};, await , console., tools. or =>) is handed to run or exec as appropriate:

mcpx ./report.ts
mcpx 'await chrome_devtools.list_pages({})'

This is deliberately conservative: a bare word that is not a file fails as an unknown command, because guessing that mcpx serach was a snippet would replace a clear error with a baffling one.

Exit codes worth knowing

codemeaning
0success
1the command failed; the error is on stderr
2an unknown command, an unknown global flag, or a settings conflict — including a contradiction in a config file, which is reported once, before dispatch
75EX_TEMPFAIL: a call stopped to ask a question (an elicitation). Not a failure, and not a cue to retry: the call continues as a task — answer with mcpx elicit answer …, then collect the original call’s result with mcpx task result <id>. Retrying would start a second call

The man page and shell completions

Both are generated from the same command declarations as everything else, so they cannot drift from the binary:

mcpx man
mcpx completion bash   # also: zsh, fish

The Nix package installs both for you: the man page to share/man/man1/mcpx.1, and completions for all three shells.

CLI arguments

Flags come in two kinds, and most confusion about mcpx flags is really about which kind one is looking at.

Kind 1: hand-parsed global flags, before the subcommand

A short list, parsed by hand in cmd/mcpx/main.go before any subcommand is chosen. These, and only these, have fixed global meanings:

flagwhat it does
--jsonone machine-readable document instead of human output
--tuiopen the TUI browser (also a subcommand)
--profile <a,b>add profiles to the active set
--skip-defaultnarrow the active set to exactly the named profiles
--all-profilesactivate every profile
--config <file>use this config file alone
--version, -vprint the version and exit
--help, -hprint help and exit

One extension to that list: a setting that every command accepts is also accepted as a global flag, so mcpx --plumbing-strict-unknown-keys ls means what mcpx ls --plumbing-strict-unknown-keys does. An unrecognised global flag is an error (exit 2), never a guess.

Kind 2: registry flags, bound per command

Every other flag is generated from the settings registry and bound per command: parseFlags calls BindFlags for the command being run, which binds exactly the settings that command reads (internal/cli/root.go, internal/settings/bind.go). A command accepts the settings it reads — no more, no less — so the flag list of mcpx run and the flag list of mcpx ls are different lists, on purpose. Flags for internals are marked [plumbing] in help rather than hidden.

Three consequences worth knowing:

  • The full inventory is generated, not listed here. Every setting, its flag, its environment variable, its default and its scope are in configuration.md, which a test keeps identical to the registry; the environment names are in environment.md. mcpx help <command> shows one command’s own flags; mcpx settings shows the resolved values and where each came from.
  • Aliases are spellings of one setting. A setting may publish shorter aliases (--runtime for --script-runtime). Giving two spellings of the same setting on one command line is an error that names both — they are the same setting, and mcpx will not guess which value you meant.
  • Flag names are derived, not chosen per setting. The flag is derived mechanically from the setting’s dotted path (daemon.leaseTTL becomes --daemon-lease-ttl), so the spelling in configuration.md is authoritative; do not guess a spelling the table does not show.

Where a value can come from at all — built-in default, config files, environment, presets, flags — and which source wins, is the precedence chain in configuration.md (“Where a value can come from”). This page only adds: a contradiction between sources that the chain cannot resolve is reported once, up front, with exit code 2, rather than by whichever command happens to read the value first.

CLI commands

The command table is generated, and this page does not copy it. Every command is declared once in Commands() (internal/cli/manual.go) — name, aliases, group, usage, detail, examples — and from that declaration come mcpx help, the man page, the shell completions, and the generated CLI commands table in parity.md, which lists each command beside the operation it reaches. A test (TestEveryDispatchedCommandIsDeclared) holds the declaration and the binary’s dispatch table equal, in both directions: no dispatched command is missing from the list, and nothing on the list fails to run. That parity is recent — serve, api, registry, adapter, openapi, man and completion were once dispatched but undeclared, and therefore invisible to help, the man page and completions; the test exists so that cannot happen silently again.

What prose can usefully add over the generated table is the grouping mcpx help presents, and what each group is for:

groupwhat is in it
runningdoing the work: run, exec, scripts, skills, recipes, prompt, proxy, embeddings, feedback
discoveryfinding out what exists before paying for its schemas: ls, catalog, types, search, call, batch, client, schema, prompts, resources, openapi
daemonthe daemon’s own lifecycle: status, daemons, refresh, reload, upgrade, restart, stop, supervise, daemon, serve
configurationshaping mcpx itself: config, settings, servers, secrets, init, registry, api, adapter, plus help, man and completion
inspectionlooking at what happened: diagnose, elicit, doctor, tui, explore, dashboard, interactions, log, stats
operationsone generated command per /v1 operation (for example mcpx artifact list) — these exist the moment an operation is declared, with flags from its parameters

For any one command, mcpx help <command> is the reference: it is printed from the same declaration the table is generated from, so the two cannot disagree.

The compendium: structure and mapping

created:      2026-10-29T19:00:00-05:00
last-updated: 2026-10-29T19:00:00-05:00
increment:    1
status:       standard
tags:         area:docs, area:compendium
description:  what the compendium is, the chapters in order, which file is the
              source of each, and the rule that keeps a file in exactly one.

The compendium is level 3 of the book: the encyclopedia. Level 1 is the overview, level 2 is the guides, and level 3 is where “basically everything that is built can be described here in as much detail as you want”. It is deliberately not limited to mcpx: note 88 asks it to catalogue MCP servers, resources, indexes, URLs and projects as well (chapter 7).

This page is the structure. The per-file mapping is compendium.manifest.json, next to it, and a test keeps the two in step (see Keeping this true).

Approach: the docs are the book source

Approach C, as recommended on #1867 and not reopened here:

  • Source root pkgs/mcpx/docs. Files render where they already are.
  • Navigation is generated from compendium.manifest.json, not written by hand and not kept in a second tree.
  • README.md stays a symlink to docs/story.md (git mode 120000), and the <!-- table-of-contents-marker --> stays where it is. The runbook’s check test -L README.md && readlink README.md keeps passing.
  • One home per document. Other levels link to a chapter or excerpt it; they do not copy it. That rule is what makes a level-3 page safe to be as long as it needs to be.

Approaches A (move the files under a book directory) and B (one stub page per document) were rejected on #1867 for the reasons recorded there: A breaks every relative link, internal/settings/docs_test.go, and the symlink; B needs a stub per document plus a check that the stubs have not drifted, and breaks links a different way.

Chapters, in order

A chapter’s source is a file or a directory. When it is a directory, its README.md is the chapter index and the rest of the directory is the chapter’s pages; a generated navigation therefore has to accept a directory, which is the concrete requirement the three in-flight directories from the 2026-09-30 inventory turned into.

#ChapterSourceHolds
1Featuresdocs/story.md, below <!-- table-of-contents-marker -->the feature list: one page per key-block entry or area: tag, split at build time at section IDs (#151). The walk above the marker is the level-1 overview (#157), not a chapter
2Designsdocs/elicitation.md, exec.md, consumer.md, opencode-plugin.md, plugin-discovery.md, environment.md, runtimes.md, schema-validation.mdthe “why” documents: a design kept as the record of a decision, at full depth
3Decision recordsdocs/decisions/ (directory) + declared-but-unread.md, landing.md, git-discovery.mdone record per collision, following the template in decisions/README.md. The two files at docs/ root record a decision each and say so in their own front matter; the chapter index links them
4Protocoldocs/protocol.md + docs/spec/ (directory)protocol.md is the design and the entry point; spec/README.md is the chapter index for the records under it: what each revision asks of each side, what mcpx sends, and how it is measured
5Build and packagingdocs/container.md, dependencies.mdwhat the binary and the image are made of, and what they deliberately are not
6Proposeddocs/proposals.mdopen designs, badged proposed. A reader deciding whether mcpx will do something next starts here
7Cataloguedocs/catalogue/ (directory)things outside mcpx: MCP servers, resources, indexes and registries, URLs, projects. Structure below; the entries are seeded by the follow-up task
8Comparisons and historydocs/compare/ (directory) + ASSESSMENT.md, OPENCODE-V2.mdthe living comparison and the historical documents, kept as history with their text intact (#153)
9Research and findingsdocs/audit/ (directory) + in-name-only.md, work-strategies.md, research-and-findings.mdaudits and lessons: what was found, not what was designed. The research chapter (#1871) has joined; its Nix section is still a later slice
10Maintaining these docsdocs/release-runbook.md, compendium.mdhow story.md stays true, the key-block rules, and this structure
11Document historydocs/archive.mdretired entries and refactored-out text, append-only and unordered (#150)

The migration mapping

Every one of the 96 markdown files under pkgs/mcpx at origin/main is in the manifest, with a chapter or an explicit reason not to have one. The 2026-09-30 inventory on #1867 covered 20-odd of them; the seven it never saw now land as:

filelands in
docs/archive.md11 Document history
docs/environment.md2 Designs (what mcpx tells a program it starts, #172)
docs/git-discovery.md3 Decision records (it calls itself a decision record, and says why it is not in decisions/)
docs/in-name-only.md9 Research and findings (the curated list of what only exists on paper)
docs/runtimes.md2 Designs
docs/schema-validation.md2 Designs
docs/work-strategies.md9 Research and findings (lessons learned)
docs/research-and-findings.md9 Research and findings (the #1871 chapter: findings by topic, provenance-tagged)
docs/audit/ (3 files)9 Research and findings
docs/compare/ (18 files)8 Comparisons and history
docs/decisions/ (5 files)3 Decision records
docs/spec/ (14 files)4 Protocol

Explicitly not compendium, and why

These are in the manifest with role: not-compendium, so the mapping is complete rather than merely plausible:

file(s)why not
docs/ideas.mdraw unvetted intake (“some of it is probably wrong”). The proposed chapter publishes the index-of-issues version instead; see open question 3
plugin/opencode/skills/** (12 files)skill text shipped inside the plugin and rendered as level-2 pages (#148). Restating it in the compendium would make two sources for one text
daemon/README.mdan operational how-to for the systemd user unit: level 2
internal/mcpspec/schema/README.mda provenance note for vendored schemas; it documents a tree, not a design
TODO.mda scratch list

Four more are role: included: AGENTS.md (the agent instruction file, linked and included but chapterless, per #1867), docs/configuration.md and docs/parity.md (reference, #144, and level 2, #146 — the compendium links to them; internal/settings/docs_test.go reads configuration.md), and plugin/opencode/README.md (install, level 2, agent twin #120).

README.md is in the manifest as a symlink entry rather than a source, so the generator has an explicit instruction about it instead of a rule it has to remember.

The catalogue chapter

Structure only in this change; the entries are a follow-up so that this page can be reviewed as a structure rather than as a directory listing.

  • Source docs/catalogue/, with a chapter README.md and one page per section: servers.md, registries.md, resources.md, urls.md, projects.md.
  • Generated or written: C. A hand-written list says which servers, registries, resources, URLs and projects are in the catalogue and why; the facts per server (mcpx --json ls tool counts, mcpx types tool lists, mcpx --json registry search publisher and version) are captured into a committed snapshot by a refresh script. Nothing at docs build time starts a server or calls the network, because CI must not have to start seven MCP servers to render a page.
  • First entries are the seven servers configured on this machine — codebase-memory, codedb, fff, fff-nix, fff-worktree, chrome-devtools (the session-mode case) and context7 — plus the official registry registry.modelcontextprotocol.io, the recorded rejection (HAPI), and the projects list from #1867. Scope beyond that machine is open question 5.

Approach A (hand-written entries only) and B (mirror mcpx registry search) stay rejected for the reasons on #1867; C is recorded here so the follow-up does not have to re-argue it.

Open questions on #1867, answered

  1. Split story.md per entry, or one long page? Split, at build time, at section IDs (#151). One page with good anchors is readable, but the feature list is also what level-2 pages excerpt (#146-#148) and what the catalogue and the decision records link to by name; a stable page per entry gives those links a target that survives an edit elsewhere in the list. The split pages are generated and not committed, so story.md and the symlinked README.md stay one file on GitHub.
  2. Move ASSESSMENT.md and OPENCODE-V2.md into docs/, or include them from the root? Move them into docs/. Both are chapter sources for chapter 8, both are linked by relative path from story.md and proposals.md, and a chapter whose sources live in two trees needs every link rewritten twice. AGENTS.md stays at the root: it is the agent instruction file and the name is the convention.
  3. ideas.md: unpublished, raw, or an index of issues? Publish the index-of-issues version in the proposed chapter; keep ideas.md itself as raw intake and do not publish it. It says of itself that it anticipates becoming “an index of links” once issues exist, and they do.
  4. Which “resources” and which “indexes” does note 88 mean? All of them, and the chapter separates them instead of choosing: resources.md covers MCP resources (the protocol feature, and what the configured servers actually expose) and reading material as its own section, while registries.md covers registries and the code indexes the configured servers maintain (fff, codedb, codebase-memory). A reader looking for either arrives at the same page.
  5. Catalogue scope: this machine’s servers, or also the ones mcpx was tested against? This machine’s seven first, because each has a recorded reason to be configured. Servers mcpx was merely tested against (the Swagger petstore in OPENCODE-V2.md, io.github.microsoft/playwright-mcp in story.md) go in a clearly marked second section, since the reason they are listed is different: evidence, not configuration.
  6. Do stale facts in living documents get fixed before the compendium publishes them? Yes, as a separate small fix, not inside this structure change: dependencies.md (“One.”), the story.md “Nix packaging” entry (vendorHash = null, “one third-party Go dependency”), and the missing nix/darwin-module.nix that ASSESSMENT.md points at. Publishing them is what turns a known staleness into a citable error. Tracked as its own follow-up, and deferred to #1907 if it is not picked up first.

Constraint this structure keeps

The one test that pins a doc path is internal/settings/docs_test.go, which reads ../../docs/configuration.md. Nothing here moves a file, so it is untouched. No relative link changes either: this change adds two files under docs/ and edits nothing else, which is also why the story.md links written relative to docs/ (../AGENTS.md, ../ASSESSMENT.md, ../OPENCODE-V2.md, ./release-runbook.md, ../scripts/stress.sh) keep resolving. When open question 2 is implemented and the two root documents move into docs/, those links change in the same pull request.

Keeping this true

internal/settings/compendium_test.go walks every markdown file under pkgs/mcpx and fails when:

  • a file is not in the manifest at all (a new document must be placed, and the failure names it);
  • a manifest path does not exist (a moved or deleted document leaves a chapter pointing at nothing);
  • an entry’s chapter is not one of the chapters listed here;
  • a chapter-source entry has no chapter, or a not-compendium entry has one without saying why.

That is the whole mechanism: adding a document to pkgs/mcpx is not finished until it has a chapter, or a reason for not having one.

Related: #1867 (this structure), #1907 (level-3 tracker), #117 (generator source root), #150/#151 (section IDs and split), #153 (chapters 8), #154/#1871 (chapter 9), #157 (level-1 overview), #144/#146-#148 (reference and level 2), #120 (agent twin), #24 (elicitation as a design).

Configuration

Every number in mcpx is a setting, including the ones nobody will ever change.

That sounds like an overreaction until you have hit the alternative. A constant written next to the code that needs it is fine right up to the day somebody’s proxy kills an idle event stream at thirty seconds, or their sandbox refuses to exec the running binary, or a tool legitimately takes a five-megabyte argument. At that point the only remedies are a patched binary or a fork, and both are absurd outcomes for a number that was chosen in five seconds and never revisited. Declaring it costs one line and removes that outcome entirely.

So the rule is: nothing is hardcoded. A value lives in internal/defaults/defaults.json as data, and – wherever a user could conceivably want it different – it is declared once in the settings registry, from which its configuration key, its environment variable, its command-line flag, its /v1 representation and its MCP tool are all derived. There is no second place to update, which is the point: the arrangement this replaced had four, and they drifted.

The handful of exceptions are permission bits. 0700 on the state directory and 0600 on the socket are not preferences. The socket grants the power to run tools as you; a configuration key that widens it would be a footgun with no legitimate use. They are in defaults.json as data so there is still one place to read them, and they are deliberately absent from the registry.

A guard test enforces this. It matches inline durations in both spellings gofmt produces, byte sizes written as shifts, bounds assigned to variables whose names say they are bounds, and permission literals, with an allowlist that carries a reason for each exemption. The version it replaced matched fixed strings and missed every duration gofmt had compacted into a function argument – nine of them, including the daemon’s shutdown grace and the event stream’s keepalive.

Where a value can come from

Lowest to highest:

  1. default – internal/defaults/defaults.json, embedded in the binary.
  2. file – every configuration file on the search path, ranked by distance, nearest highest. mcpx config --sources lists them.
  3. env – MCPX_*. Every setting has one, derived from its path, whether or not anybody uses it.
  4. flag – the command line.
  5. runtime – an override set through PUT /v1/settings/{path} or mcpx settings set. Highest because it is the most recent statement of intent: somebody changed their mind while the thing was running, and having a flag from ten minutes ago win would be inexplicable.

mcpx settings get <path> prints the winning value, where it came from, and what it displaced. “Why is this not what my config says” is the most common configuration question there is, and the answer is always in that list.

The search path itself is decided before any of those layers exist, in two passes. The discovery pass walks the default locations, seeded by MCPX_PATHS_CONFIG / --paths-config, and reads only paths.config. The load pass resolves that list and loads everything on it, nearest last: a file’s paths.config governs the rest of the chain from that point, a null entry stands for the locations the default walk provided, and a list with no null replaces the farther remainder. A paths.config found only in the load pass is reported by mcpx config --sources, not applied, and a cycle is refused naming the path that closed it. MCPX_CONFIG bypasses both passes and loads exactly one file.

Two spellings of one setting at the same level is an error rather than a race: --log-level and --logging-level together, or two variables that mean the same thing with different values. The environment has no order, so there is genuinely nothing to prefer, and picking one silently is how a configuration becomes unexplainable.

Scope: who reads it

Every setting says which process acts on it. This exists because of a failure with no symptom.

A daemon is started once, in whatever environment it happened to have, and then serves every command for hours. A flag on one of those commands that governs something the daemon does was simply lost: accepted, validated, printed back, and then ignored, because the process that would act on it had read its configuration an hour earlier. Nothing anywhere said so.

  • daemon – the pooled servers, the listener, the log the daemon writes. A client changes one by persisting it and restarting, or through mcpx settings set if the setting is hot.
  • client – the CLI process itself: how it renders, how it finds a daemon, whether it may start one. Setting it on the daemon does nothing.
  • call – meaningful per request. The CLI sends its effective value with every request in an X-Mcpx-Settings header, and the daemon honours it for that request only. This is what makes mcpx search --limit 3 work against a daemon that was started without it. Only call-scoped settings are honoured from that header: widening it would make a header a way for any client to reconfigure a shared daemon.
  • plugin – read by the opencode plugin, which is neither. The plugin reads process.env directly, so the variable names are the contract between the two and a test pins them.

Hot: does it take effect now

A hot setting is read afresh every time it is used, so changing it at runtime works. A cold one was consumed once at startup – a listener address, a log file that is already open – and recording a change to it would do nothing. PUT /v1/settings/{path} says so rather than accepting a value that would be silently inert.

Reaching them

surfacereadchange
config fileany mcpx config on the search pathedit it
environmentMCPX_*export it
CLImcpx settings list, mcpx settings get <path>mcpx settings set <path> <value> [--persist runtime|project|user], mcpx settings unset <path>
/v1GET /v1/settings, GET /v1/settings/{path}PUT /v1/settings/{path}, DELETE /v1/settings/{path}
MCPmcpx_settings_list, mcpx_settings_getmcpx_settings_set, mcpx_settings_unset

mcpx settings list resolves locally, which is what the client will use. --daemon asks the daemon what it resolved, which is what a daemon-scoped setting actually is. The two differing is information, not a bug: it is exactly the case that used to be invisible.

--persist runtime changes the running daemon and nothing on disk, which is what an experiment wants. project and user write the corresponding configuration file and apply at once where the setting allows it – a change written to a file but not applied would leave mcpx settings get answering with the old value, which reads as the write having failed.

Local-model controls by surface

These are independent controls, not one fleet-wide Qwen switch. In particular, parking hosted chat-model listings does not disable embeddings or an interpreter. The checked-in state below is from this repository’s main configuration, not a guarantee about a running process: nearer files, environment, flags and runtime overrides can change it. See #2218 for the remaining single-switch work.

SurfaceControl and checked-in stateWhat it does not control
mcpx catalog embeddingsembeddings.backend defaults to auto; .mcpx.json sets no embeddings block. With no URL or WASM path configured there, the cascade reaches the built-in pure-Go subword engine, not a Qwen model. local forces that engine.Prompt generation, OpenCode messaging and provider listings. auto can use a configured remote or WASM model; it is not a global local-model off switch.
mcpx prompt generationprompt.sample defaults to never; model.provider defaults to none. Neither is set in .mcpx.json. The file does contain a legacy repair block naming opencode and claude-code/claude-haiku-5-5, not Qwen.Embeddings or arbitrary models chosen by a caller or external OpenCode.
OpenCode messaging embeddings.opencode/opencode.jsonc sets messaging.embedding to "off". Removing that setting restores the OpenCode default, which starts its Qwen3-Embedding local server.Chat provider listings or mcpx embeddings.
OpenCode chat provider listingsQwen definitions for cline-pass and commandcode are parked as comments; other Qwen/Llama listings are under disabled providers or commented blocks.Messaging embeddings. Restoring the parked blocks requires editing them: there is not yet a single switch restoring all listings.
OpenChamber route interpreterThe separate local route-interpreter stage is controlled in OpenChamber Settings; its Qwen2.5 model requires a download and was reported absent on the laptop in the #2218 audit.OpenCode provider listings and mcpx. This is a separate cross-repository remainder, not inherited state.

For catalog embeddings, auto tries the configured embeddings.url remote endpoint, then a successfully loaded embeddings.wasmPath, then the subword engine. remote and wasm each fall back to subword on failure; local uses subword directly. A remote endpoint may itself be local (for example Ollama): the backend name does not say where the model runs. embeddings.model names the remote model, not a local download. These settings select an embedder; they do not switch semantic search itself off.

For prompt generation, prompt.sample: never prevents an unmatched-recipe request from asking a model by default; ask allows it. An explicit request provider or model, or a non-none model.provider, also opts in even when prompt.sample is never. model.provider selects the newer model layer (none, auto, sampling, plugin, openai, anthropic, or google), with model.baseURL and model.default for a configured endpoint and model. An OpenAI-compatible local endpoint can serve Qwen, but it must be configured separately. model.provider: none alone is not a universal veto: the legacy scripting provider can resolve an explicit request, environment or installed CLI when generation is allowed. The legacy repair.provider and repair.model keys in the fleet file are not registered settings in this version, so do not assume that block selects the effective provider. Per-request mcpx prompt --provider / --model can also select a provider. None of these controls constrain independent callers of external services.

Use mcpx settings get embeddings.backend --daemon, mcpx settings get prompt.sample --daemon, and mcpx settings get model.provider --daemon to inspect the daemon’s resolved settings; check mcpx config --sources and the daemon environment as well. Embedding backends are constructed when settings are installed, so verify the running daemon after changing configuration rather than assuming an existing index or embedder has switched. Model provider and prompt sampling settings are daemon-scoped and cold.

Servers

Servers are configuration too, and until now adding one meant an editor and a restart.

surfacelistaddremove
CLImcpx servers listmcpx servers add <name> -- <cmd>..., mcpx registry add <n> --writemcpx servers remove <name>
/v1GET /v1/serversPOST /v1/serversDELETE /v1/servers/{name}
MCPmcpx_servers_listmcpx_servers_addmcpx_servers_remove

The entry is written to a configuration file and the daemon reloads, so the server is callable immediately and is still there tomorrow. A server whose process definition did not change keeps its running child: adding one server must not restart the rest, or a stateful server – a browser holding a session – loses it because a neighbour appeared.

The daemon also re-reads its configuration files when they change on disk, checked on the same tick that reaps idle instances (daemon.watchConfig). An edit by hand therefore takes effect within daemon.reapInterval without anything being restarted.

Making that work required changing what identifies a daemon. The socket used to be keyed to the contents of the config files, so that editing one got a fresh daemon rather than a stale one. A daemon that reloads changes the contents of the files it is keyed by, which moves its own key, which makes it unreachable by the client that just edited it – and hands the next command a different daemon that happens to match the new key with old servers loaded. That was observed: mcpx servers remove succeeded, the file was correct, and the next mcpx servers list showed the removed server. The key is now the file set, and staleness is handled where it belongs.

Hiding tools

tools (an allowlist) and excludeTools (a denylist) go in a server’s mcpx block, or in the top-level pool block to apply to every server. A hidden tool is left out of every listing – mcpx tools, generated types, search, the MCP pass-through – and a call to it by name is refused with 400.

{
  "pool": { "excludeTools": ["delete_*"] },
  "mcpServers": {
    "datadog": { "command": "mcp-remote", "args": ["..."], "mcpx": { "excludeTools": ["execute_*", "/^(update|upsert)_.*dashboard/"] } }
  }
}

Each entry is one of:

entrymeansexample
/.../a Go regular expression, unanchored as regexps are/delete/, /^delete_/
contains *, ? or [a glob over the whole name: * any run, ? one character, [a-z] / [!a-z] a classdelete_*, *_monitor
anything elsethe exact tool namedelete_datadog_workflow

A pattern that cannot compile is a configuration error naming the field, not a rule that silently matches nothing.

Constraining calls by their arguments

Hiding decides by tool name: a tool is callable with any arguments or not at all. argumentRules decides by the call: a per-server map in the mcpx block – also allowed in the top-level pool block – whose keys are tool patterns matched exactly as tools/excludeTools match (exact name, glob, or /regexp/), and whose values are JSON Schema objects. A call is allowed only if its arguments validate against every rule whose key matches the tool. Pool rules and server rules add up, and so do rules from layered config files: rules only tighten, and a nearer file cannot loosen a farther one.

{
  "mcpServers": {
    "github": {
      "command": "...",
      "mcpx": {
        "argumentRules": {
          "create_repository": {
            "required": ["private", "organization"],
            "properties": {
              "private": { "const": true },
              "organization": { "pattern": "^(my-org|my-org-sandbox)$" }
            }
          },
          "/^(push_files|create_or_update_file|delete_file)$/": {
            "required": ["branch"],
            "properties": { "branch": { "not": { "enum": ["main", "master"] } } }
          },
          "*": {
            "properties": { "owner": { "pattern": "^(my-org|my-user)$" } }
          }
        }
      }
    }
  }
}

Per-argument rules are the same mechanism: a "*" key whose schema is a properties object constrains that argument on every tool that carries it, and leaves tools without the argument untouched.

A refused call is a 400, as a hidden tool is, before the server is started: the message names the argument path, the value sent, the constraint it broke, and where the rule came from (config file, server, key). Over the MCP pass-through the same refusal arrives as a tool result with isError: true. Listings advertise the tightened schema: the inputSchema shown by mcpx types, the generated client, search, and the pass-through’s tools/list is the upstream schema allOf every matching rule, so an agent sees private: true before it ever calls.

A rule whose key matches no tool on its server warns at load and on catalog changes – a server that renames a tool must not silently disable the rule that guarded it. A value that is not a valid schema, or a pattern the validator cannot compile (JSON Schema patterns are ECMA-262; lookahead and backreferences are unsupported), is a configuration error naming the server and key, like a bad tool pattern.

Two cautions, plainly:

  • Server defaults are invisible. A rule constrains what is sent, not what the server fills in. If a constraint matters, list the argument in required – never rely on a server-side default to satisfy it.
  • Free-form arguments cannot truly be constrained. A pattern over a query/jql/sql string matches text, not meaning. A tool whose danger lives in a free-form argument belongs in excludeTools, not in a rule.

extraIncludeTools takes the same entries and adds them to the allowlist instead of replacing it, so a server can widen a pool-wide tools by one tool without restating the rest:

{
  "pool": { "tools": ["get_*", "list_*", "search_*"] },
  "mcpServers": {
    "datadog": { "mcpx": { "extraIncludeTools": ["create_datadog_monitor"] } }
  }
}

With no tools anywhere, extraIncludeTools is the allowlist – it includes, as tools does, so everything it does not name is hidden.

excludeTools wins over both. A server’s own tools replaces the pool’s; extraIncludeTools and excludeTools from the pool and the server all apply, so a server that hides one more tool does not undo a pool-wide rule. A nearer config file’s pool.extraIncludeTools likewise adds to a farther one’s. An alias (aliasOf) filters independently of the server it shares a process with.

Names are the server’s own tool names, as mcpx tools prints them in its tool column, not the generated function names.

Some servers can filter on their side too – Datadog’s omit_tools URL parameter, GitHub’s GITHUB_READ_ONLY – which keeps the tool from ever reaching mcpx. Those are each server’s own convention; this one works for any server.

Write policy (policy.write)

A tool call is a write when its tool is not annotated readOnlyHint true, read with the specification’s defaults: an unannotated tool may write, and may be destructive. The daemon-wide setting policy.write (default ask) gates every such call, between resolving an instance and using it:

modewhat happens to a write call
aska confirmation is raised showing the exact call – the tool and its arguments in canonical (key-sorted) JSON – and the call proceeds only if somebody answers true
allowthe call proceeds with no confirmation from the policy
denythe call is refused, with an error naming the tool, the mode, and the settings that can change it

A server’s own writePolicy in its mcpx block overrides the daemon mode for that server alone:

{ "mcpServers": { "datadog": { "command": "...", "mcpx": { "writePolicy": "deny" } } } }

Under ask, a caller that cannot answer questions is refused, not waited out and not let through: the error says the policy is ask, that there is no question broker to answer it, and that setting policy.write to allow (or deny) is the escape. An unanswered or refused confirmation refuses the call too.

elicit.confirmDestructive is a floor on top of this, not an alternative to it: with it on, a destructive call asks even under allow – raised by the destructive confirmation itself, with its annotation-trust rules unchanged – and the floor never weakens a deny. Under ask the policy has already asked once, so no call is asked about twice.

This gate is an operator consent gate, not the autonomy dial (docs/decisions/0002-autonomy-dial.md): the dial bounds what mcpx does on its own, the write policy bounds what a caller may do to the outside world through it, and neither clamps the other.

Every setting

Everything between the markers below is generated from settings.Registry() by TestTheDocumentationListsEverySetting, which fails when it is stale:

go test ./internal/settings -run TestTheDocumentationListsEverySetting -update

It used to say it was generated while being written by hand, and it had drifted: two settings were missing an alias their flag really answers to. mcpx config --schema --plumbing prints the same inventory, and mcpx settings list prints it with the values that are actually in force.

Entries marked (plumbing) are internals. They work and they are supported, but there is no ordinary reason to change one, so they are kept out of ordinary help rather than hidden – a flag a user can find in the source and that --help denies exists is worse than a long list.

paths.configFile is the one setting a configuration file may not set: it decides which file is read, so it is accepted from the environment and from the global --config, which goes before the command.

plumbing.strictUnknownKeys governs keys no setting claims, and keys a server entry declares that mcpx does not know — type, alwaysAllow and the rest, which belong to the other hosts that read the same file. Those are recorded, reported by mcpx doctor, and refused only when this is on.

A key inside a server’s "mcpx" block is not governed by it and is always refused. That block is mcpx’s alone — nothing else writes into it — so a key it does not know is a typo or a name that no longer exists, not another host’s field. This is deliberately not a setting: it is how mcpx init wrote "mode": "session" into every new config for months without anyone noticing, and a switch to turn the report off would have kept it quiet.

A script whose first line names its interpreter (#!) runs under that interpreter, ahead of every setting below: a -S/--split-string payload is re-split with shell quoting rules (so -S sh -c "printf hello" keeps printf hello one argument; an unterminated quote fails the run), VAR=value assignments reach the child even when the value contains /, env -u/env -i apply (the child starts from the parent’s environment minus what they remove, or from an empty one), and env -C/env --chdir moves the run, beating script.cwd the way the interpreter choice beats script.runtime. An env option mcpx does not implement fails the run rather than being dropped. A dispatched run never inherits MCPX_CLIENT, MCPX_RUNTIME, MCPX_ENTRY or MCPX_DISPATCH from an outer run, and its notice names anything else it goes without (no generated client, no enforced permission profiles, skipped launcher phases).

artifacts

settingkinddefaultscopehotflagvariablegoverns
artifacts.chunkBytesbytes256KiBdaemonno--artifacts-chunk-bytesMCPX_ARTIFACTS_CHUNK_BYTES(plumbing) how much of a body one streamed frame carries
artifacts.deliveryenumreferencecallyes--artifacts-deliveryMCPX_ARTIFACTS_DELIVERYhow artifact bodies reach the caller
artifacts.dirstring(empty)callyes--artifacts-dirMCPX_ARTIFACTS_DIRwrite this run’s artifacts here
artifacts.enabledbooltruedaemonno--artifacts-enabledMCPX_ARTIFACTS_ENABLEDwhether the daemon keeps files scripts produce
artifacts.gcIntervalduration10mdaemonno--artifacts-gc-intervalMCPX_ARTIFACTS_GC_INTERVAL(plumbing) how often expired artifacts are swept
artifacts.idleTtlduration0sdaemonno--artifacts-idle-ttlMCPX_ARTIFACTS_IDLE_TTLwhen set, reading an artifact renews it for this long; off keeps absolute expiry
artifacts.inlineMaxBytesbytes1MiBcallyes--artifacts-inline-max-bytesMCPX_ARTIFACTS_INLINE_MAX_BYTESthe largest artifact that may be base64’d into a result
artifacts.interceptImagesbooltruecallyes--artifacts-intercept-imagesMCPX_ARTIFACTS_INTERCEPT_IMAGESturn image and audio content in a result into artifact references
artifacts.maxAgeduration168hdaemonno--artifacts-max-ageMCPX_ARTIFACTS_MAX_AGEthe absolute ceiling on a renewed artifact’s life, however often it is read
artifacts.maxBytesbytes64MiBdaemonno--artifacts-max-bytesMCPX_ARTIFACTS_MAX_BYTESthe largest single artifact that may be stored
artifacts.quotabytes1GiBdaemonno--artifacts-quotaMCPX_ARTIFACTS_QUOTAthe total the artifact store may hold
artifacts.ttlduration24hdaemonno--artifacts-ttlMCPX_ARTIFACTS_TTLhow long an artifact is kept before it is collected

autonomy

settingkinddefaultscopehotflagvariablegoverns
autonomy.maxenumrundaemonno--autonomy-maxMCPX_AUTONOMY_MAXthe most autonomy this daemon exercises, whatever a caller asks for

autostart

settingkinddefaultscopehotflagvariablegoverns
autostart.argslistdaemon,--detachedclientno--autostart-argsMCPX_AUTOSTART_ARGS(plumbing) the arguments an auto-started daemon is given
autostart.binstring(empty)clientno--autostart-binMCPX_AUTOSTART_BINwhich executable is started as the daemon
autostart.connectTimeoutduration20sclientno--autostart-connect-timeoutMCPX_AUTOSTART_CONNECT_TIMEOUThow long to wait for a started daemon to answer
autostart.idleExitduration4h0m0sclientno--autostart-idle-exitMCPX_AUTOSTART_IDLE_EXIThow long an auto-started daemon survives with nothing to do
autostart.logTailbytes2000clientno--autostart-log-tailMCPX_AUTOSTART_LOG_TAIL(plumbing) how much of the daemon log is shown when it will not start
autostart.pingTimeoutduration2sclientno--autostart-ping-timeoutMCPX_AUTOSTART_PING_TIMEOUT(plumbing) how long a health check waits before calling it dead
autostart.pollIntervalduration50msclientno--autostart-poll-intervalMCPX_AUTOSTART_POLL_INTERVAL(plumbing) how often a starting daemon is probed

catalog

settingkinddefaultscopehotflagvariablegoverns
catalog.biaslist(empty)callyes--catalog-bias, --biasMCPX_CATALOG_BIASwords that pull matching tools toward the front
catalog.budgetint2000callyes--catalog-budget, --budgetMCPX_CATALOG_BUDGETtoken ceiling for the catalog listing
catalog.instructionsbooltruecallyes--catalog-instructionsMCPX_CATALOG_INSTRUCTIONSinclude each server’s own instructions
catalog.pinnedToolslist(empty)daemonno--catalog-pinned-toolsMCPX_CATALOG_PINNED_TOOLStools always exposed in reactive mode

completion

settingkinddefaultscopehotflagvariablegoverns
completion.maxValuesint100daemonyes--completion-max-valuesMCPX_COMPLETION_MAX_VALUES(plumbing) how many completions one reply carries
completion.perMinuteint120daemonyes--completion-per-minuteMCPX_COMPLETION_PER_MINUTE(plumbing) how many completion requests one caller may make against one server per minute; 0 disables
completion.serverPerMinuteint600daemonyes--completion-server-per-minuteMCPX_COMPLETION_SERVER_PER_MINUTE(plumbing) how many completion requests one server may receive per minute across all callers; 0 disables

daemon

settingkinddefaultscopehotflagvariablegoverns
daemon.addressstring127.0.0.1daemonno--daemon-address, --addressMCPX_DAEMON_ADDRESSwhich interface the daemon listens on
daemon.autostartbooltrueclientno--daemon-autostartMCPX_DAEMON_AUTOSTARTstart the daemon on demand when it is not running
daemon.endpointstring(empty)clientno--daemon-endpointMCPX_DAEMON_ENDPOINTa daemon somewhere else, instead of the local socket
daemon.idleExitduration0sdaemonno--daemon-idle-exit, --idle-exitMCPX_DAEMON_IDLE_EXITstop the daemon after this long with nothing to do
daemon.inlineboolfalseclientno--daemon-inlineMCPX_DAEMON_INLINEas a last resort, run servers inside this process
daemon.inlineStartPollduration20msclientno--daemon-inline-start-pollMCPX_DAEMON_INLINE_START_POLL(plumbing) how often a starting in-process daemon is probed
daemon.inlineStartTimeoutduration5sclientno--daemon-inline-start-timeoutMCPX_DAEMON_INLINE_START_TIMEOUT(plumbing) how long an in-process daemon has to become reachable
daemon.leaseTTLduration30m0sdaemonyes--daemon-lease-ttlMCPX_DAEMON_LEASE_TTL(plumbing) how long a silent caller’s instances are remembered
daemon.portint0daemonno--daemon-port, --portMCPX_DAEMON_PORTlisten on a TCP port instead of choosing one
daemon.probeTimeoutduration200msclientno--daemon-probe-timeoutMCPX_DAEMON_PROBE_TIMEOUT(plumbing) how long another daemon’s socket is given to answer
daemon.reapIntervalduration30sdaemonno--daemon-reap-intervalMCPX_DAEMON_REAP_INTERVAL(plumbing) how often idle instances are swept
daemon.refreshTimeoutduration3m0sdaemonyes--daemon-refresh-timeoutMCPX_DAEMON_REFRESH_TIMEOUThow long POST /v1/refresh may take
daemon.rootspaths(empty)daemonno--daemon-rootsMCPX_DAEMON_ROOTSfilesystem paths exposed to upstream servers through roots/list
daemon.saveIntervalduration5mdaemonno--daemon-save-intervalMCPX_DAEMON_SAVE_INTERVAL(plumbing) how often daemon state is written to disk
daemon.socketProbeTimeoutduration500msdaemonno--daemon-socket-probe-timeoutMCPX_DAEMON_SOCKET_PROBE_TIMEOUT(plumbing) how long a socket left by a crashed daemon is given to answer
daemon.takeoverTimeoutduration15sdaemonno--daemon-takeover-timeoutMCPX_DAEMON_TAKEOVER_TIMEOUT(plumbing) how long a handoff may take, and how long the old daemon waits for its successor to hang up
daemon.tsnet.hostnamestring(empty)daemonno--daemon-tsnet-hostnameMCPX_DAEMON_TSNET_HOSTNAMEalso serve the daemon on a tailnet, under this name
daemon.tsnetUpTimeoutduration30sdaemonno--daemon-tsnet-up-timeoutMCPX_DAEMON_TSNET_UP_TIMEOUT(plumbing) how long a node may take to authenticate
daemon.uibooltruedaemonyes--daemon-ui, --uiMCPX_DAEMON_UIserve the dashboard page at /dashboard
daemon.uiDirstring(empty)daemonyes--daemon-ui-dir, --ui-dirMCPX_DAEMON_UI_DIRserve your own pages instead of the embedded dashboard
daemon.warmbooltruedaemonno--daemon-warm, --warmMCPX_DAEMON_WARMread every server’s schemas in the background at startup
daemon.warmTimeoutduration3m0sdaemonno--daemon-warm-timeoutMCPX_DAEMON_WARM_TIMEOUT(plumbing) how long the background schema fetch may take
daemon.watchConfigbooltruedaemonyes--daemon-watch-configMCPX_DAEMON_WATCH_CONFIGre-read the configuration files when they change on disk

deps

settingkinddefaultscopehotflagvariablegoverns
deps.autoInstallenumoffclientno--deps-auto-installMCPX_DEPS_AUTO_INSTALLwhether the runtime may fetch a missing package on its own
deps.manifestsource(empty)clientno--deps-manifestMCPX_DEPS_MANIFESTthe package.json or package.jsonc naming a script’s packages
deps.packageslist(empty)clientno--deps-packages, --packageMCPX_DEPS_PACKAGESpackages a script may import, as name or name@version
deps.sourceenumhostclientno--deps-sourceMCPX_DEPS_SOURCEwhere a script’s packages come from when nothing more specific says

diagnose

settingkinddefaultscopehotflagvariablegoverns
diagnose.historyint8daemonno--diagnose-historyMCPX_DIAGNOSE_HISTORY(plumbing) how many changes are kept per tool
diagnose.preflightbooltruecallyes--diagnose-preflightMCPX_DIAGNOSE_PREFLIGHTcheck a script’s tool calls against the live schemas first

doctor

settingkinddefaultscopehotflagvariablegoverns
doctor.timeoutduration20sclientno--doctor-timeoutMCPX_DOCTOR_TIMEOUThow long mcpx doctor gives the daemon to answer

elicit

settingkinddefaultscopehotflagvariablegoverns
elicit.askTimeoutduration45sdaemonno--elicit-ask-timeoutMCPX_ELICIT_ASK_TIMEOUThow long mcpx waits for an answer to a question it raised itself
elicit.confirmDestructiveboolfalsedaemonno--elicit-confirm-destructiveMCPX_ELICIT_CONFIRM_DESTRUCTIVEask before a call to a tool that may be destructive
elicit.disambiguateenumneverdaemonno--elicit-disambiguateMCPX_ELICIT_DISAMBIGUATEask which instance of a stateful server to use when several are live and the script did not choose
elicit.disambiguateDefaultenumnewdaemonno--elicit-disambiguate-defaultMCPX_ELICIT_DISAMBIGUATE_DEFAULTwhich instance is used when nobody answers in time: the one the scope would have chosen, or the most recently used
elicit.maxAgeduration30mdaemonno--elicit-max-ageMCPX_ELICIT_MAX_AGEthe absolute ceiling on a question’s life, however often it is read
elicit.pendingLimitint100daemonyes--elicit-pending-limitMCPX_ELICIT_PENDING_LIMIT(plumbing) how many unanswered questions one listing returns
elicit.ttlduration120sdaemonno--elicit-ttlMCPX_ELICIT_TTLhow long a question waits for an answer, renewed by use

embeddings

settingkinddefaultscopehotflagvariablegoverns
embeddings.apiKeystring(empty)daemonyes--embeddings-api-keyMCPX_EMBEDDINGS_API_KEYAPI key for remote embeddings endpoint
embeddings.backendstringautodaemonyes--embeddings-backendMCPX_EMBEDDINGS_BACKENDwhich embedding engine to use (auto, local, remote, wasm)
embeddings.modelstringtext-embedding-3-smalldaemonyes--embeddings-modelMCPX_EMBEDDINGS_MODELmodel name for remote embeddings endpoint
embeddings.urlstring(empty)daemonyes--embeddings-urlMCPX_EMBEDDINGS_URLOpenAI-compatible embeddings endpoint URL
embeddings.wasmPathstring(empty)daemonyes--embeddings-wasm-pathMCPX_EMBEDDINGS_WASM_PATHpath to compiled WebAssembly neural embedding model

events

settingkinddefaultscopehotflagvariablegoverns
events.historyint1024daemonno--events-historyMCPX_EVENTS_HISTORYhow many past events a late subscriber can replay
events.reconnectduration2sclientno--events-reconnectMCPX_EVENTS_RECONNECThow long a client waits before resuming a dropped stream

exec

settingkinddefaultscopehotflagvariablegoverns
exec.maxOutputBytesbytes64KiBcallyes--exec-max-output-bytesMCPX_EXEC_MAX_OUTPUT_BYTEScap the stdout a run returns, with a truncation marker
exec.outputenumtextcallyes--exec-output, --outputMCPX_EXEC_OUTPUTtext for a terminal, structured for one document, stream for frames
exec.timeoutduration120scallyes--exec-timeoutMCPX_EXEC_TIMEOUTkill a script after this long
exec.whereenumautocallyes--exec-whereMCPX_EXEC_WHEREin this process, or on the daemon

hooks

settingkinddefaultscopehotflagvariablegoverns
hooksstring(empty)daemonno--hooksMCPX_HOOKSconfig-declared hooks over the event bus: a JSON array of hook objects
hooks.autonomyenumruncallyes--hooks-autonomyMCPX_HOOKS_AUTONOMYwhether configured script hooks (before, onSuccess, onError) run
hooks.timeoutduration2sdaemonno--hooks-timeoutMCPX_HOOKS_TIMEOUTthe hard timeout for a declared hook that does not set its own

http

settingkinddefaultscopehotflagvariablegoverns
http.bodyLimitbytes1048576daemonyes--http-body-limitMCPX_HTTP_BODY_LIMIT(plumbing) the ceiling on an ordinary /v1 request body
http.callBodyLimitbytes67108864daemonyes--http-call-body-limitMCPX_HTTP_CALL_BODY_LIMITthe ceiling on a POST /v1/call body
http.controlBodyLimitbytes65536daemonyes--http-control-body-limitMCPX_HTTP_CONTROL_BODY_LIMIT(plumbing) the ceiling on a body that should hold one field
http.idleConnTimeoutduration30sclientno--http-idle-conn-timeoutMCPX_HTTP_IDLE_CONN_TIMEOUT(plumbing) how long an unused connection is kept
http.idleConnsint8clientno--http-idle-connsMCPX_HTTP_IDLE_CONNS(plumbing) idle connections kept to a local daemon
http.readHeaderTimeoutduration10sdaemonno--http-read-header-timeoutMCPX_HTTP_READ_HEADER_TIMEOUT(plumbing) how long a client has to finish sending its headers
http.remoteIdleConnsint16clientno--http-remote-idle-connsMCPX_HTTP_REMOTE_IDLE_CONNS(plumbing) idle connections kept to a daemon over the network
http.requestTimeoutduration10m0sclientno--http-request-timeoutMCPX_HTTP_REQUEST_TIMEOUThow long a CLI request to the daemon may take
http.shutdownGraceduration5sdaemonyes--http-shutdown-graceMCPX_HTTP_SHUTDOWN_GRACEhow long in-flight requests have when the daemon stops
http.ssePingduration15sdaemonyes--http-sse-pingMCPX_HTTP_SSE_PING(plumbing) how often a comment is sent on an idle event stream
http.sseRetryduration2sdaemonyes--http-sse-retryMCPX_HTTP_SSE_RETRY(plumbing) how long a dropped subscriber is told to wait
http.streamBufferInitbytes65536clientno--http-stream-buffer-initMCPX_HTTP_STREAM_BUFFER_INIT(plumbing) the initial line buffer when reading an event stream
http.streamBufferMaxbytes8388608clientno--http-stream-buffer-maxMCPX_HTTP_STREAM_BUFFER_MAX(plumbing) the largest single event line that will be read

logging

settingkinddefaultscopehotflagvariablegoverns
logging.dirstring(empty)daemonno--logging-dir, --log-dirMCPX_LOGGING_DIRwhere the JSONL files are written
logging.filebooltruedaemonno--logging-fileMCPX_LOGGING_FILEwhether the durable JSONL log is written at all
logging.formatenumtextclientno--logging-format, --formatMCPX_LOGGING_FORMAThow records are rendered
logging.includelisthost,user,process,versiondaemonno--logging-include, --includeMCPX_LOGGING_INCLUDEwhich context blocks are attached to lifecycle records
logging.keepint8daemonno--logging-keep, --keepMCPX_LOGGING_KEEPhow many rolled files to keep
logging.levelenuminfodaemonyes--logging-level, --log-levelMCPX_LOGGING_LEVEL, MCPX_LOG_LEVELthe lowest level that is kept
logging.maxAgeduration24hdaemonno--logging-max-ageMCPX_LOGGING_MAX_AGEroll the log file once it is this old
logging.maxBytesbytes16MBdaemonno--logging-max-bytesMCPX_LOGGING_MAX_BYTESroll the log file once it reaches this size
logging.maxLinesint0daemonno--logging-max-linesMCPX_LOGGING_MAX_LINESroll the log file once it holds this many records
logging.sourceenumwarndaemonyes--logging-source, --log-sourceMCPX_LOGGING_SOURCEfrom which level upward to record the calling file and line
logging.traceboolfalsedaemonno--logging-traceMCPX_TRACElog which instance served every tool call

logstore

settingkinddefaultscopehotflagvariablegoverns
logstore.followBacklogint1000clientno--logstore-follow-backlogMCPX_LOGSTORE_FOLLOW_BACKLOG(plumbing) how many records one poll of mcpx log --follow may emit
logstore.queryLimitint100callyes--logstore-query-limitMCPX_LOGSTORE_QUERY_LIMIThow many records a log query returns by default

mcp

settingkinddefaultscopehotflagvariablegoverns
mcp.pageSizeint100clientno--mcp-page-sizeMCPX_MCP_PAGE_SIZEhow many items one tools/list reply carries
mcp.passthroughstring(empty)clientno--mcp-passthrough, --passthroughMCPX_MCP_PASSTHROUGHserve upstreams’ tools, prompts and resources under their own names

model

settingkinddefaultscopehotflagvariablegoverns
model.apiKeyEnvstring(empty)daemonno--model-api-key-envMCPX_MODEL_API_KEY_ENVthe environment variable holding the configured provider’s API key
model.baseURLstring(empty)daemonno--model-base-urlMCPX_MODEL_BASE_URLthe OpenAI-compatible endpoint the configured provider calls
model.budgetint0daemonno--model-budgetMCPX_MODEL_BUDGEThard ceiling on the tokens one generation through the layer may ask for; 0 is no ceiling
model.defaultstring(empty)daemonno--model-defaultMCPX_MODEL_DEFAULTthe model id used when an operation names none, as provider/model
model.providerenumnonedaemonno--model-providerMCPX_MODEL_PROVIDERwhere mcpx’s own generation gets a model: a configured provider, sampling through the caller, or none
model.timeoutduration1m30sdaemonno--model-timeoutMCPX_MODEL_TIMEOUThow long one generation through the layer may take

output

settingkinddefaultscopehotflagvariablegoverns
output.colorenumautoclientno--output-colorMCPX_OUTPUT_COLORwhether to colourise the browser
output.jsonboolfalseclientno--output-json, --jsonMCPX_OUTPUT_JSONemit one machine-readable document

paths

settingkinddefaultscopehotflagvariablegoverns
paths.adapterspaths(empty)clientno--paths-adaptersMCPX_PATHS_ADAPTERSfiles declaring command-line programs as MCP servers
paths.apispaths(empty)clientno--paths-apisMCPX_PATHS_APISfiles naming OpenAPI documents to expose as tools
paths.cachestring(empty)clientno--paths-cacheMCPX_PATHS_CACHE, MCPX_CACHE_DIRwhere generated clients and schemas are kept
paths.configpaths(empty)clientno--paths-configMCPX_PATHS_CONFIGwhere configuration files are looked for
paths.configFilestring(empty)clientno--configMCPX_CONFIGread exactly this configuration file instead of searching
paths.depspaths(empty)clientno--paths-depsMCPX_PATHS_DEPSwhere a script’s package.json / package.jsonc is looked for
paths.placeholderspaths(empty)clientno--paths-placeholdersMCPX_PATHS_PLACEHOLDERSdirectories of files declaring launcher placeholders
paths.pluginspaths(empty)clientno--paths-pluginsMCPX_PATHS_PLUGINSdirectories of plugins: an mcpx-plugin.json manifest, or executables loaded as zero-manifest plugins
paths.scriptspaths(empty)clientno--paths-scriptsMCPX_PATHS_SCRIPTSwhere named scripts are looked for
paths.statestring(empty)clientno--paths-stateMCPX_PATHS_STATE, MCPX_STATE_DIRwhere the daemon socket, logs and index live

plugin

settingkinddefaultscopehotflagvariablegoverns
plugin.annotatebooltruepluginno--plugin-annotateMCPX_PLUGIN_ANNOTATEadd mcpx’s own notes to a tool result
plugin.backendenumautopluginno--plugin-backendMCPX_PLUGIN_BACKENDwhether the plugin talks to the daemon or spawns the binary
plugin.binstringmcpxpluginno--plugin-binMCPX_PLUGIN_BINthe executable the plugin invokes
plugin.binArgslist(empty)pluginno--plugin-bin-argsMCPX_PLUGIN_BIN_ARGSarguments put in front of every plugin invocation of mcpx
plugin.daemonToolsenumautopluginno--plugin-daemon-toolsMCPX_PLUGIN_DAEMON_TOOLSoffer the tools that list and choose between daemons
plugin.discoveryRetryduration1m0spluginno--plugin-discovery-retryMCPX_PLUGIN_DISCOVERY_RETRYhow long the plugin waits before looking for mcpx again
plugin.envenumfullpluginno--plugin-envMCPX_PLUGIN_ENVhow much session context is put into each command’s environment
plugin.headlessenumautopluginno--plugin-headlessMCPX_PLUGIN_HEADLESSsuppress toasts, because nobody is looking at a screen
plugin.instructionsboolfalsepluginno--plugin-instructionsMCPX_PLUGIN_INSTRUCTIONSadd mcpx usage guidance to the system prompt
plugin.rememberenumsessionpluginno--plugin-rememberMCPX_PLUGIN_REMEMBERhow long a chosen daemon stays chosen
plugin.toolTimingboolfalsepluginno--plugin-tool-timingMCPX_PLUGIN_TOOL_TIMINGwrite every tool outcome into mcpx’s log
plugin.toolsboolfalsepluginno--plugin-toolsMCPX_PLUGIN_TOOLSexpose mcpx itself as tools the model can call

plumbing

settingkinddefaultscopehotflagvariablegoverns
plumbing.allowTsJsOverlapboolfalseclientno--plumbing-allow-ts-js-overlapMCPX_PLUMBING_ALLOW_TS_JS_OVERLAP(plumbing) permit a script directory holding both foo.ts and foo.js
plumbing.indexOnQuerybooltruedaemonno--plumbing-index-on-queryMCPX_PLUMBING_INDEX_ON_QUERY(plumbing) bring the log index up to date before answering a query
plumbing.launcherPlaceholderRepeatlist(empty)clientno--plumbing-launcher-placeholder-repeatMCPX_PLUMBING_LAUNCHER_PLACEHOLDER_REPEAT(plumbing) launcher placeholders permitted to resolve more than once
plumbing.sourceDirAllowedbooltrueclientno--plumbing-source-dir-allowedMCPX_PLUMBING_SOURCE_DIR_ALLOWED(plumbing) whether a directory may stand in for a source string at all
plumbing.sourceDirRecursiveboolfalseclientno--plumbing-source-dir-recursiveMCPX_PLUMBING_SOURCE_DIR_RECURSIVE(plumbing) when a directory is given as source, descend into subdirectories
plumbing.sourceProbePathsbooltrueclientno--plumbing-source-probe-pathsMCPX_PLUMBING_SOURCE_PROBE_PATHS(plumbing) treat a source argument that names an existing file as a file
plumbing.strictUnknownKeysboolfalseclientno--plumbing-strict-unknown-keysMCPX_PLUMBING_STRICT_UNKNOWN_KEYS(plumbing) fail on a configuration key no setting claims
plumbing.validatePathsbooltrueclientno--plumbing-validate-pathsMCPX_PLUMBING_VALIDATE_PATHS(plumbing) resolve and verify every referenced path before doing any work

policy

settingkinddefaultscopehotflagvariablegoverns
policy.writeenumaskdaemonno--policy-writeMCPX_POLICY_WRITEwhat to do before a call that may write: ask for confirmation, allow it, or deny it

pool

settingkinddefaultscopehotflagvariablegoverns
pool.callTimeoutduration120sdaemonyes--pool-call-timeoutMCPX_POOL_CALL_TIMEOUThow long one tool call may take
pool.idleTimeoutduration5mdaemonyes--pool-idle-timeoutMCPX_POOL_IDLE_TIMEOUThow long an unused server lingers before it is stopped
pool.maxint4daemonyes--pool-maxMCPX_POOL_MAXhow many copies of one server may run at once
pool.minint0daemonyes--pool-minMCPX_POOL_MINhow many copies to keep started even when idle
pool.scopeenumglobaldaemonno--pool-scopeMCPX_POOL_SCOPEwhat counts as the same caller for sharing purposes
pool.sharingenumshareddaemonno--pool-sharingMCPX_POOL_SHARINGwhether callers reuse one instance or each get their own
pool.startTimeoutduration60sdaemonyes--pool-start-timeoutMCPX_POOL_START_TIMEOUThow long a server has to become ready

preset

settingkinddefaultscopehotflagvariablegoverns
presetlist(empty)clientno--presetMCPX_PRESETwhich presets to apply, in order

presets

settingkinddefaultscopehotflagvariablegoverns
presetsstring(empty)clientno--presetsMCPX_PRESETSnamed flag bundles: {name: [“–json”, “–timeout=30s”]}

prompt

settingkinddefaultscopehotflagvariablegoverns
prompt.autonomyenumproposecallyes--prompt-autonomyMCPX_PROMPT_AUTONOMYwhether a request returns the script (propose) or runs it (run)
prompt.catalogBudgetint1200callyes--prompt-catalog-budgetMCPX_PROMPT_CATALOG_BUDGETapproximate token ceiling for the slice of catalog sent with a sampling request
prompt.maxTokensint1500callyes--prompt-max-tokensMCPX_PROMPT_MAX_TOKENS(plumbing) maxTokens on the sampling request
prompt.runTimeoutduration5m0scallyes--prompt-run-timeoutMCPX_PROMPT_RUN_TIMEOUThow long a recipe or generated script may run
prompt.sampleenumneverdaemonno--prompt-sampleMCPX_PROMPT_SAMPLEwhether a request with no matching recipe may ask the caller’s model to write one
prompt.sampleTimeoutduration1m30sdaemonno--prompt-sample-timeoutMCPX_PROMPT_SAMPLE_TIMEOUThow long a generation request waits for a model

proto

settingkinddefaultscopehotflagvariablegoverns
proto.askPollduration500msdaemonno--proto-ask-pollMCPX_PROTO_ASK_POLL(plumbing) how long one wait for a question may block
proto.askRoundsint8daemonno--proto-ask-roundsMCPX_PROTO_ASK_ROUNDShow many times one request may come back asking for more
proto.askTTLduration15mdaemonno--proto-ask-ttlMCPX_PROTO_ASK_TTL(plumbing) how long the daemon keeps a call waiting for an answer
proto.askTimeoutduration10mdaemonno--proto-ask-timeoutMCPX_PROTO_ASK_TIMEOUThow long one request may be held while a question goes unanswered
proto.elicitationPerMinuteint30daemonno--proto-elicitation-per-minuteMCPX_PROTO_ELICITATION_PER_MINUTEmax elicitation requests one upstream server may make per minute, across all its sessions
proto.mcpPathstring/mcpdaemonno--proto-mcp-pathMCPX_PROTO_MCP_PATHwhere the daemon serves MCP
proto.nativebooltruedaemonno--proto-nativeMCPX_PROTO_NATIVEput an upstream server’s questions to mcpx’s own MCP client
proto.samplingPerMinuteint60daemonno--proto-sampling-per-minuteMCPX_PROTO_SAMPLING_PER_MINUTEmax sampling requests one upstream server may make per minute, across all its sessions
proto.samplingSessionPerMinuteint20daemonno--proto-sampling-session-per-minuteMCPX_PROTO_SAMPLING_SESSION_PER_MINUTEmax sampling requests one upstream server may make within a single session per minute
proto.serveMCPbooltruedaemonno--proto-serve-mcpMCPX_PROTO_SERVE_MCPmount mcpx’s own MCP server on the daemon’s listeners
proto.sessionIdleduration30mdaemonno--proto-session-idleMCPX_PROTO_SESSION_IDLE(plumbing) how long an unused Streamable HTTP session is kept
proto.stateTTLduration30mdaemonno--proto-state-ttlMCPX_PROTO_STATE_TTL(plumbing) how long a client may resume an interrupted request with

protoMessages

settingkinddefaultscopehotflagvariablegoverns
protoMessages.listMaxAgeduration1m0sdaemonno--proto-messages-list-max-ageMCPX_PROTO_MESSAGES_LIST_MAX_AGEttlMs on server/discover and every list result sent to a 2026-07-28 client
protoMessages.readMaxAgeduration0sdaemonno--proto-messages-read-max-ageMCPX_PROTO_MESSAGES_READ_MAX_AGEttlMs on resources/read results sent to a 2026-07-28 client
protoMessages.taskAfterduration2sdaemonno--proto-messages-task-afterMCPX_PROTO_MESSAGES_TASK_AFTERhow long a tools/call runs in line before a tasks-extension client is handed a task

protoTasks

settingkinddefaultscopehotflagvariablegoverns
protoTasks.cancelGraceduration5sdaemonno--proto-tasks-cancel-graceMCPX_PROTO_TASKS_CANCEL_GRACEhow long cooperative task work may finish after cancellation
protoTasks.maxPerRequestorint32daemonno--proto-tasks-max-per-requestorMCPX_PROTO_TASKS_MAX_PER_REQUESTORhow many non-terminal tasks one requestor may hold at once
protoTasks.maxTTLduration1h0m0sdaemonno--proto-tasks-max-ttlMCPX_PROTO_TASKS_MAX_TTLthe longest TTL a task may ask for; longer requests are clamped
protoTasks.perMinuteint120daemonno--proto-tasks-per-minuteMCPX_PROTO_TASKS_PER_MINUTEthe tasks/* admission window per requestor; 0 disables it
protoTasks.pollIntervalduration1sdaemonno--proto-tasks-poll-intervalMCPX_PROTO_TASKS_POLL_INTERVALthe pollInterval every task carries

recipes

settingkinddefaultscopehotflagvariablegoverns
recipes.limitint5callyes--recipes-limitMCPX_RECIPES_LIMIThow many ranked recipes are returned
recipes.matchMarginint150callyes--recipes-match-marginMCPX_RECIPES_MATCH_MARGINhow far ahead the best recipe must be, as a percentage of the next one
recipes.minScoreint30callyes--recipes-min-scoreMCPX_RECIPES_MIN_SCOREthe score below which a request is not considered a match for any recipe

registry

settingkinddefaultscopehotflagvariablegoverns
registry.limitint20callyes--registry-limitMCPX_REGISTRY_LIMIThow many registry results are returned
registry.maxPagesint10clientno--registry-max-pagesMCPX_REGISTRY_MAX_PAGES(plumbing) how many requests one registry search may make
registry.pageSizeint30clientno--registry-page-sizeMCPX_REGISTRY_PAGE_SIZE(plumbing) how many entries are fetched per registry request
registry.timeoutduration30sclientno--registry-timeoutMCPX_REGISTRY_TIMEOUThow long a registry request may take
registry.urlstringhttps://registry.modelcontextprotocol.ioclientno--registry-urlMCPX_REGISTRY_URLwhere mcpx registry looks for servers

repair

settingkinddefaultscopehotflagvariablegoverns
repair.autonomyenumadvisecallyes--repair-autonomyMCPX_REPAIR_AUTONOMYhow far mcpx goes about a failed call: off attaches nothing, advise attaches diagnostics

retention

settingkinddefaultscopehotflagvariablegoverns
retention.maxToolsint24daemonno--retention-max-toolsMCPX_RETENTION_MAX_TOOLSmaximum active tools in reactive working set
retention.stickyWindowduration2hdaemonno--retention-sticky-windowMCPX_RETENTION_STICKY_WINDOWduration explicitly requested tools remain retained

script

settingkinddefaultscopehotflagvariablegoverns
script.beforesource(empty)clientno--script-beforeMCPX_SCRIPT_BEFOREruns first, ahead of the globals being installed
script.captureConsolebooltrueclientno--script-capture-consoleMCPX_SCRIPT_CAPTURE_CONSOLEroute console calls into the log
script.cwdenumcallerclientno--script-cwdMCPX_SCRIPT_CWDwhere a script runs: caller, script, config or workdir
script.envlist(empty)clientno--script-env, --envMCPX_SCRIPT_ENVKEY=VALUE pairs added to the script’s environment
script.importsenumreportclientno--script-importsMCPX_SCRIPT_IMPORTSresolve the script’s import graph before running it
script.importsOnDynamicenumwarnclientno--script-imports-on-dynamicMCPX_SCRIPT_IMPORTS_ON_DYNAMICwhat to do about an import() whose specifier is computed
script.interpreterstring(empty)clientno--script-interpreter, --interpreter, --shellMCPX_SCRIPT_INTERPRETERcommand line that runs scripts instead of a JavaScript runtime
script.launchersource(empty)clientno--script-launcherMCPX_SCRIPT_LAUNCHERreplace the generated launcher entirely
script.onErrorsource(empty)clientno--script-on-errorMCPX_SCRIPT_ON_ERRORruns when the script throws; the error still propagates
script.onSuccesssource(empty)clientno--script-on-successMCPX_SCRIPT_ON_SUCCESSruns when the script returns without throwing
script.permissionsstringallclientno--script-permissions, --permissionsMCPX_SCRIPT_PERMISSIONS, MCPX_PERMISSIONSpermission profiles, composed in order, or raw: flags
script.prefixsource(empty)clientno--script-prefixMCPX_SCRIPT_PREFIXruns after globals are installed, before the script
script.profilesstring(empty)clientno--script-profilesMCPX_SCRIPT_PROFILESuser profiles: {name: {deno: [flags], node: [flags]}} or {name: “raw flags”}
script.runtimestringautoclientno--script-runtime, --runtimeMCPX_SCRIPT_RUNTIMEwhich JavaScript runtime executes the script
script.runtimeOrderlistdeno,bun,nodeclientno--script-runtime-orderMCPX_SCRIPT_RUNTIME_ORDERthe runtimes auto tries, in order
script.runtimesstring(empty)clientno--script-runtimesMCPX_SCRIPT_RUNTIMESnamed runtime binaries: {name: {kind, bin, args}}
script.suffixsource(empty)clientno--script-suffixMCPX_SCRIPT_SUFFIXruns last on both paths, like a finally
script.typecheckenumoffclientno--script-typecheckMCPX_SCRIPT_TYPECHECKcheck the generated program before running it
settingkinddefaultscopehotflagvariablegoverns
search.limitint20callyes--search-limit, --limit, --nMCPX_SEARCH_LIMIThow many tools a search returns
search.semanticboolfalsecallyes--search-semanticMCPX_SEARCH_SEMANTICrank search results using local dense vector embeddings

serve

settingkinddefaultscopehotflagvariablegoverns
serve.modestringcode-modedaemonno--serve-mode, --modeMCPX_SERVE_MODEserver mode: code-mode, reactive, or full

session

settingkinddefaultscopehotflagvariablegoverns
session.releaseTimeoutduration10sclientno--session-release-timeoutMCPX_SESSION_RELEASE_TIMEOUT(plumbing) how long releasing a finished session may take

spec

settingkinddefaultscopehotflagvariablegoverns
spec.firststring(empty)daemonno--mcp-specMCPX_MCP_SPECmove one MCP revision to the front of spec.precedence
spec.lenientlist(empty)daemonno--spec-lenientMCPX_SPEC_LENIENTMCP revisions not held strictly; every revision not listed is strict
spec.precedencelist2026-07-28,2025-11-25,2025-06-18,2025-03-26,2024-11-05daemonno--spec-precedenceMCPX_SPEC_PRECEDENCEMCP revisions in the order their rules win a conflict, first wins

stats

settingkinddefaultscopehotflagvariablegoverns
stats.topint20callyes--stats-topMCPX_STATS_TOPhow many rows a ranked statistic shows

tasks

settingkinddefaultscopehotflagvariablegoverns
tasks.resultWaitduration2m0scallyes--tasks-result-waitMCPX_TASKS_RESULT_WAIThow long collecting a task result blocks before giving up
tasks.ttlduration10m0sdaemonyes--tasks-ttlMCPX_TASKS_TTLhow long a finished task’s result is kept

transport

settingkinddefaultscopehotflagvariablegoverns
transport.allowedOriginslist(empty)daemonno--transport-allowed-originsMCPX_TRANSPORT_ALLOWED_ORIGINSbrowser Origins, beyond loopback, that may reach /mcp
transport.bearerTokenstring(empty)daemonno--transport-bearer-tokenMCPX_TRANSPORT_BEARER_TOKENwhen set, every /v1 and /mcp request must carry this token as a Bearer credential
transport.sessionMetaKeyslistdev.mcpx/session,ai.opencode/sessionIDdaemonyes--transport-session-meta-keysMCPX_TRANSPORT_SESSION_META_KEYSthe _meta keys a client may name its session with, first present wins
transport.sseKeepAliveduration15sdaemonno--transport-sse-keep-aliveMCPX_TRANSPORT_SSE_KEEP_ALIVEhow often a quiet MCP event stream carries a comment line
transport.stdioDrainduration30sclientno--transport-stdio-drainMCPX_TRANSPORT_STDIO_DRAINhow long mcpx serve finishes in-flight requests after its input closes

upstream

settingkinddefaultscopehotflagvariablegoverns
upstream.eraCachebooltruedaemonno--upstream-era-cacheMCPX_UPSTREAM_ERA_CACHEremember which era each server configuration speaks, across restarts
upstream.probeTimeoutduration2sdaemonno--upstream-probe-timeoutMCPX_UPSTREAM_PROBE_TIMEOUThow long a stdio server/discover may go unanswered before initialize is also sent
upstream.protocolenumprefer-discoverdaemonno--upstream-protocolMCPX_UPSTREAM_PROTOCOLwhich request to send first to a server that names no protocol of its own; one of prefer-discover (also modern, prefer-modern, prefer-stateless, prefer-newest), prefer-initialize (also legacy, prefer-legacy, prefer-session, prefer-oldest), force-discover (also force-modern, force-stateless), force-initialize (also force-legacy, force-session), follow

Adapter declarations

An entry in a paths.adapters file names a program and the subcommands worth exposing. command is normally a binary on PATH; runtime makes it a script instead, and resolves the interpreter for you:

{ "adapters": [{
  "name": "reports", "command": "./tools/report.py", "runtime": "py",
  "tools": [{ "name": "daily", "params": [{ "name": "day", "required": true }] }]
}] }

runtime is bash, js, ts or py. bash resolves bash (then sh), py resolves python3 (then python), and js/ts use the same deno/bun/node order script.runtimeOrder chooses for mcpx exec. This is a separate resolver on purpose: script.runtime names the runtime mcpx itself executes generated scripts on, not the one a declaration asks for. A relative command resolves against the declaration’s dir, then PATH. Without runtime, nothing changes: command is looked up on PATH and exec’d directly. mcpx adapter check reports a missing interpreter or script.

mcpx catalog also ends with the recipe tools in tools/list: how many are exposed against the recipe budget, and which recipes were kept out by a name collision or the cap.

mcpx as a consumer

Sampling, elicitation and completion are things a server asks of its client. mcpx is a client to its upstream servers and a server to whatever drives it, so today it only passes them through. Are there cases where mcpx should initiate them itself?

— issue #41

Four, and they are not equally good. This is the record of which, why, and what was deliberately not built.

1. Deterministic schema-change diagnostics

The most valuable thing here needs no model at all, and that was the central finding of the issue. mcpx already knows every tool’s input schema. What it did not know was what those schemas used to be — internal/codegen could fingerprint a catalog and diff two of them, but nothing ever stored one, so every diff was against nothing and the question “when did this change” had no answer anywhere in the program.

So internal/diagnose keeps one. Each time the daemon reads schemas, it records every tool’s flattened shape and appends whatever differs to a per-tool change list in <state>/catalog-history.json. That file is the missing half. With it, a script that calls a tool with no arguments after that tool gained a required one is not a mystery; it is two facts mcpx is holding, put next to each other:

demo.echo: the argument object is no longer optional: message is required
           (schema changed 2026-09-20: `message` became required)
  you wrote:  await demo.echo()   (line 2)
  minimum:    await demo.echo({ message: "" })

Three decisions inside that are worth stating.

The scanner does not parse TypeScript. It looks for two identifiers, a dot and a parenthesis, and skips every construct that could hide one: strings, template literals, both comment forms. A call assembled dynamically is invisible to it, and that is the correct failure — a diagnostic about a call nobody wrote is worse than no diagnostic. It also stops at the first sign of not knowing: a spread in an argument object silences the missing-argument check entirely, because a spread can supply anything.

Only configured namespaces are diagnosed. console.log(x) and JSON.parse(s) are indistinguishable from tool calls at this level. Having an opinion about somebody else’s code is not this package’s business.

The issue’s example needed correcting. It reads:

demo.create_issue: argument 1 is now required (`options` became required).
  you wrote:  await demo.create_issue("title")
  minimum:    await demo.create_issue("title", {})

mcpx’s generated client does not take positional arguments. codegen binds every tool as fn(args?: {...}) — one object, optional exactly when the schema has no required properties. So the diagnostic mcpx can actually produce is “the argument object is no longer optional”, which is the same fact in the shape the generated code has. Anything else would be advice that does not compile.

The upstream half, diagnose.CallError, runs in Registry.Call (internal/daemon/callerr.go), the one place a failed call passes on its way to every surface. The server’s error stays first and whole; the rendered diagnostic follows it, and /v1/call carries the same finding as data:

{ "error": "mcp error -32602: Invalid params\ndemo.create_issue: the call was rejected and repo is required but was not sent (schema changed 2026-09-30: `repo` was added and is required)\n  minimum:    await demo.create_issue({ repo: \"\", ... })", "diagnostics": [{ "kind": "invalid-params", "tool": "demo.create_issue", "field": "repo", "changed": { "when": "2026-09-30T…", "kind": "argument-added", "what": "`repo` was added and is required" }, "fix": "await demo.create_issue({ repo: \"\", ... })", "fatal": true }] }

So mcpx call prints it, mcpx --json call puts the document on stdout, a task’s result carries it, a script’s ToolError has .diagnostics, and mcpx_call shows the text. Data beside the prose, because an agent can act on field where it would have to parse a sentence.

Three conditions decide whether it runs, and each is there because of what it said without them. Only an answer from a server is explained: a timeout or a server that would not start never answered, so the arguments were not at fault. Not a code in -32000..-32099: mcpx’s own client reports a dropped connection as -32000 connection closed: unexpected EOF, and the text match read “unexpected” as “expected” — a crash was explained as a missing argument. And not a namespace whose schemas have not been read, where CallError said “no server is configured under this namespace” about a configured server; it used to answer that for any failure, before it looked at the code at all.

Not covered: mcpx_call from a client that can answer questions goes through CallAsk, which leases the pool itself; /v1/call/{server}/{tool} carries the text but not the field; and a server that reports bad arguments as a result with isError has made a successful call, so there is no failure to explain.

--diagnose-preflight is on by default on run and exec. The failure it catches surfaces otherwise as an error from a server, halfway through, after the side effects of every call before it.

2. Elicitation mcpx raises for itself

Two policies, both off by default, both landing on Registry.Call between resolving an instance and using it.

Disambiguation (elicit.disambiguate, never | ask). It asks only when the answer is genuinely open: the server is exclusive, so its instances are distinct things rather than copies; several are live; and the caller’s own scope key names none of them. When the caller already has an instance, the scope key is the answer and a question would be theatre. The form is an enum of the live keys plus “new”, each with what it is doing — holders, calls, idle time, pid — because a choice without that is not a choice.

Destructive confirmation (elicit.confirmDestructive, off). Before a call to a tool that may be destructive: annotated destructiveHint: true, or (the specification’s default) not annotated readOnlyHint: true or destructiveHint: false. This is the step a human-driven client has and a script does not.

The two disagree about what a silent deadline means, deliberately. An unanswered disambiguation takes elicit.disambiguateDefault, which is new — a caller that wanted a particular browser and did not say so gets a fresh one rather than somebody else’s. An unanswered confirmation refuses the call. There, doing nothing is safe; here, doing nothing is the destructive thing.

Routing needed no change in internal/elicit. A confirmation is a lone boolean called confirm, which Route already sends to a human because consent is not the agent’s to give; a disambiguation is an enum, which goes to the agent that raised the call. internal/elicit/policy.go adds only the constructors, whose job is to make it impossible to raise one of mcpx’s own questions without a default and a deadline.

The daemon had no access to settings before this — daemon.Options carries a config.Config and nothing else — so settings.Resolve folds the same configuration files and environment the CLI reads. Whether a destructive call is confirmed is not a question with two correct answers depending on which process asked.

Several daemons, the other disambiguation case in the issue, is not here. It belongs to the plugin: the question is which daemon serves a directory, and by the time mcpx is answering, that has been decided.

3. Recipes

A recipe is not a new artefact. It is a script already on the search path that declares its own holes:

// Close stale issues in a repository.
// @param repo:string        which repository, as owner/name
// @param days:number = 30   how old counts as stale

const found = await demo.search({ repo: @repo, olderThanDays: @days });

Declaration is a header comment because a comment survives every runtime, cannot affect execution, and is visible in the file somebody edits. References are @name, the spelling the launcher already uses for its own holes.

A recipe is also an MCP tool. The @param header is already a typed schema, so tools/list carries one tool per recipe, named recipe_<name>, with that schema and the summary as its description. Calling it runs the recipe through /v1/recipes/{name}/run – the same render, diagnostics and runner mcpx recipes run uses. Exposure is on by default; // @tool false in the header opts one out. The number exposed is capped (DefaultToolBudget, 25) so fifty saved scripts do not become fifty schemas in every host, and mcpx catalog ends with the count, the cap and any recipe kept out – a name another producer already claimed is reported there rather than silently dropped.

Values are substituted as JSON, never as text. A recipe runs with the user’s credentials; raw substitution would make every placeholder an injection site. A string arrives quoted, an object arrives as an object, and a recipe with an unfilled required hole is an invalid script — which is the right way round, because it cannot then be run by accident.

Matching is a score over four things the recipe already carries: its name, its summary, its placeholder names and the tools it calls. No model, no embedding, no index. The same request gives the same recipe every time, at no cost. Decide refuses to choose when the leader is not ahead of the runner-up by recipes.matchMargin (150%, half as much again), because running the wrong saved script is a side effect rather than a wrong answer.

JavaScript built-ins are excluded from the tool list. console.log counted as a tool makes every recipe match the word “log”.

Missing placeholders become one form with one deadline, not one question each: a person filling in four fields is doing one thing, and four deadlines are four chances for a run to die half-configured.

4. /prompt, which is POST /v1/intent

The name. POST /v1/prompt was taken, by the operation that renders an upstream server’s MCP prompt, and Go’s mux keys on method and path. Renaming somebody else’s operation to free a word was not worth it. The CLI command is mcpx prompt, which is what the issue asked for; the route is /v1/intent.

The order is recipes first, generation second, and generation is off by default (prompt.sample: never). The issue sequences sampling-backed generation last and conditions it on there being a caller that can answer sampling; most cannot, and a caller that cannot pays the entire deadline to find out. With it off, an unmatched request returns the ranked recipes and says so in one sentence. That is the honest default, and it is also the one that does not make mcpx prompt hang for ninety seconds on a machine with no answerer.

With it on, the request goes out as an ordinary MCP sampling request through the same broker that carries every other question — so the plugin, an agent watching mcpx elicit, and an MCP client that declared sampling all answer it without learning anything new. It is given the slice of the catalog the existing search ranked for this request, inside prompt.catalogBudget tokens, and nothing else. The whole reason mcpx is worth having is that tool schemas stay out of a model’s context; a generation request has to send some, so it sends as few as it can.

The generated script is validated with the step-1 diagnostics before anything runs, and returned rather than run unless autonomy: run is asked for.

Whatever is asked for, the daemon’s autonomy.max is the most it does: a request above it — in a body, the X-Mcpx-Settings header, the CLI’s --run, an MCP tool call or the plugin — is lowered to it, not refused, and the answer carries requested and clampedBy beside autonomy. The CLI prints requested run, clamped to propose by autonomy.max (...) on stderr. The same ceiling bounds repair.autonomy (whether a failed call carries diagnostics) and hooks.autonomy (whether configured script hooks run).

Running a script from the daemon

recipe_run and intent in run mode need to execute something, and the daemon has never run scripts. Server.execScript calls internal/runner directly, against the daemon’s own endpoint. This is a seam, not a design. POST /v1/exec has since landed, with its own options and permission model (internal/api/ops_exec.go, internal/daemon/routes_exec.go), but execScript (internal/daemon/consumer.go:517) has not yet been rerouted through it. It is one function, marked, for exactly that reason.

The concerns from the issue, and what was decided

Scope — “anything that makes mcpx call a model has to justify that it isn’t rebuilding the harness.” Three of the four things built here never call one. The fourth originally stopped there: option 3 from the issue, a configured provider with an API key, was not built, on the grounds that it is the one that makes mcpx an agent. That conclusion has since been reversed by the facts #1925 records: the 2026-07-28 MCP revision deprecates sampling (SEP-2577) and names integrating directly with provider APIs as the migration, and no opencode — the primary consumer — declares sampling at all. So a configured provider now exists, as one backend of the thin provider layer in internal/model: a Provider interface with a single Generate, reached only through the model.* settings, with model.provider defaulting to none — a user who configures nothing gets exactly the behaviour described above. Sampling through the caller survives as another backend of the same layer: it is mcpx asking the harness it already sits under, which remains right where a client declares it. What has not changed is the guard that made the original refusal coherent: no operation calls a model on a path where a deterministic answer exists, and a provider error returns the deterministic result plus a note that the model was unavailable. A Generate function is not an agent; the harness mcpx sits under still owns the loop, the tools and the session.

Trust — “generated code runs with the user’s credentials.” Return the script by default (prompt.autonomy: propose, one level of the dial in decisions/0002); run only on explicit request. Recipe values are JSON-encoded, so a recipe cannot be turned into an injection site by its arguments. A recipe name that would become a path is refused.

Latency and cost — “never sample on a path that doesn’t need it.” The deterministic diagnostics come first and answer the case the issue identified as the common one. Recipe matching is free and deterministic. Sampling happens on exactly one path, only when nothing matched, and only when it has been turned on.

Headless callers. Every question mcpx raises for itself carries a default and a deadline, and elicit.Ask exists so that forgetting either is not possible. The defaults differ by question, for the reasons above. A caller that gets no answer is told which question it raised, so it can look up what was asked rather than guessing.

Not built

  • A language service. The issue is right that suggesting a fix to a script is LSP work rather than MCP completion, and right to keep the two apart. internal/diagnose is the part an LSP would need; the server itself is a separate piece of work and nothing here presumes its shape.
  • Sampled rewrites of a diagnostic. When the caller is an agent it reads the error either way; the value is in the error being specific, not in a model rewording it. If a human-facing front end ever wants a proposed rewrite, diagnose.Diagnostic is already the structured input for one.
  • Daemon disambiguation, as above: the plugin’s question, not mcpx’s.

Bugs found on the way

The schema cache (cacheVersion) is not invalidated when a cached entry gains a field. Adding Annotations to mcpclient.Tool parsed cleanly against an existing cache file and left it empty, which presented as the destructive-call policy silently never firing — the annotations were there upstream and absent from the cache nobody had reason to reread. The version is bumped, with a comment saying why; the shape of the trap is still there for the next field.

Running mcpx in a container

What Containerfile builds, what it deliberately does not, and what was measured along the way.

id:           running-mcpx-in-a-container
since:        pre-release
created:      2026-09-30T04:00:00-05:00
last-updated: 2026-10-30T06:40:00-05:00
status:       implemented

A starting point rather than a shipping image. The goal was the smallest thing that actually works – builds, runs, and can execute a script – so that the decisions that come next are made against something real instead of a design.


1. What it is

Three stages:

stageimagewhy
buildgolang:1.27-bookwormgo.mod asks for 1.26.7; the image ships 1.27.1
bunoven/bun:1-debiana pinned bun, so the build does not depend on a release URL
runtimedebian:bookworm-slimordinary, and has the glibc the builder linked against

All three are pinned by tag and digest. The tag says what it is; the digest says which one. A tag moves, and a build that depends on when it ran is not a build.

The runtime stage adds ca-certificates, copies in bun and mcpx, drops to an unprivileged user, and sets ENTRYPOINT ["mcpx"]. It has no git; §3 says why that is not a loss.

The build context is the module directory, because the Containerfile copies go.mod and builds ./cmd/mcpx — both relative to pkgs/mcpx, not to the repository root:

cd pkgs/mcpx
docker build -f Containerfile -t mcpx .
docker run --rm mcpx --version
docker run --rm -v "$PWD/.mcpx.json:/work/.mcpx.json:ro" mcpx ls

2. The JavaScript runtime

The image ships bun. mcpx exec and mcpx run are the point of mcpx, and neither can do anything without a JavaScript runtime – runner.Detect returns no JavaScript runtime found and the command fails. An image that cannot run a script is an image that can only do the things a plain MCP client could already do.

Bun because it is the smallest of the three. Measured on this machine (linux/arm64, the binary inside each project’s own Debian image):

runtimebinary
bun 1.4.279.4 MB
deno (denoland/deno:debian)85.0 MB
node 22 (node:22-bookworm-slim)122.2 MB

Smaller margins than the x86-64 figures usually quoted, but the same order.

runner.Resolve picks the first of script.runtimeOrder (by default deno, bun, node) on PATH, so an image with only bun would find bun anyway. MCPX_SCRIPT_RUNTIME=bun is set anyway, because relying on absence to select a default means adding a second runtime later silently changes which one runs. The setting is script.runtime (--script-runtime, script.runtime in config); naming a runtime the image does not contain is an error, not a fallback, so anyone who wants deno’s permission model has to add deno to the image.

That is the one real cost of choosing bun: bun has no permission model, so any script.permissions profile other than all is refused under it (see runtimes.md). Node enforces the readnet profile through --permission; deno enforces all of them. Inside a container that is a smaller gap than it is on a laptop, but it is a gap.

3. No git

The repo and worktree sharing scopes key server instances by the repository a call came from. They used to ask git, and the first version of this image installed git so they could: 105 MB, most of it perl.

They no longer ask git. mcpx reads .git itself – a port of git’s own discovery rules, compared against git rev-parse case by case in the test suite – and runs git only for the few layouts that port does not vouch for: a repository format newer than it knows, an extension it has not heard of, a config file it cannot parse. Without git those key by directory, and mcpx doctor says which file it could not read. The full record, every case and what was decided for it, is docs/git-discovery.md.

Taking git out cost nothing, because in this image it had not been working:

$ docker run --rm -v "$PWD:/work:ro" --entrypoint git mcpx:before -C /work/proj \
    rev-parse --git-common-dir
fatal: detected dubious ownership in repository at '/work/proj'
To add an exception for this directory, call:

	git config --global --add safe.directory /work/proj

A mounted checkout belongs to the host’s uid, the image runs as 1000, and git refuses a repository someone else owns. So with git installed, the repo and worktree scopes fell back to per-directory keys for every mounted checkout – the situation the old version of this section said git was there to prevent. Native discovery does not check ownership: safe.directory exists to stop git running configuration from a repository someone else controls, and discovery runs nothing.

What was verified in the image with no git on PATH, one daemon for the whole run, a real repository with a linked worktree (made with worktree.useRelativePaths, so its paths survive the mount) and a submodule, a bare repository, and a tmpfs mounted inside the checkout:

INSTANCE  CALLS  KEY
byrepo#2  4      repo:/work/proj/.git              proj, proj/sub, proj-wt/sub, and exec from proj-wt
byrepo#3  1      repo:/work/proj/.git/modules/mod  the submodule is its own repository
bytree#2  2      worktree:/work/proj               proj and proj/sub
bytree#3  2      worktree:/work/proj-wt            proj-wt/sub and exec
bytree#4  1      worktree:/work/proj/mod
bytree#5  1      cwd:/work/bare.git                no worktree: core.bare is true in /work/bare.git/config
bytree#6  1      cwd:/work/proj/mnt/inner          discovery stops at the mount point /work/proj/mnt
tiny#1    1      global

The old image’s git, told to trust the mount with safe.directory=*, gave the same answers for the submodule, the bare repository and the mount – and refused the other three directories with unknown repository extension found: relativeworktrees, because Debian’s git is 2.39 and the checkout was made by 2.55. With GIT_DISCOVERY_ACROSS_FILESYSTEM=1 in the container’s environment the mounted directory keys to worktree:/work/proj, as it would in git. mcpx ls listed all three namespaces, call and exec (on bun) reached each, and mcpx doctor -v from the linked worktree said:

ok    git   repo /work/proj/.git, worktree /work/proj-wt; resolved natively; no git on PATH,
            which only matters for a repository native discovery cannot read

A repository that does need git is visible, not silent: the key falls back to the directory, the daemon logs why once, and doctor turns the line into a warning that says to install git. Adding it back is one package in the apt-get install line.

4. Static linking: the policy

CGO_ENABLED is unset in an ordinary Go build, which means enabled on a native build, and two packages change behaviour when it is:

  • internal/logging/trace.go:9 imports os/user, which under cgo calls getpwuid_r through libc rather than parsing /etc/passwd;
  • net is reachable from most of the tree, and under cgo its resolver is getaddrinfo, which honours nsswitch.conf.

Policy (#1987): the artifacts that ship as a bare mcpx binary ship static. The trade-off is the two bullets above — pure-Go os/user does not consult NSS and the pure-Go resolver ignores nsswitch.conf — and it is accepted for these artifacts because neither behaviour is load-bearing for a daemon/CLI whose state lives in its own directories, while a binary that only runs beside one exact glibc (or one Nix store path) fails everywhere else silently.

artifactstatic?note
Nix packageyespackage.nix sets env.CGO_ENABLED = "0"; the installed bin/mcpx is a makeBinaryWrapper launcher by design — the binary it launches is the static one
this image’s mcpxyesthe Containerfile defaults ARG CGO_ENABLED=0 and the build fails if the binary does not record the setting it was built with (go version -m)
this image as a wholenobun is a dynamically linked distribution binary; the runtime stage is debian:bookworm-slim for bun’s glibc, no longer for mcpx’s
release tarballnot decided herethe release pipeline is #1881’s; this policy is the input it should consume

Measured, not assumed (the CGO_ENABLED=0 build, before it became the default):

$ docker build -f Containerfile --build-arg CGO_ENABLED=0 -t mcpx:cgo0 .
$ docker run --rm --entrypoint sh mcpx:cgo0 -c 'ldd /usr/local/bin/mcpx'
	not a dynamic executable
$ docker run --rm mcpx:cgo0 --version
mcpx 0.1.0
$ docker run --rm -v "$PWD:/work:ro" mcpx:cgo0 \
    exec 'const r = await tiny.echo({ message: "cgo0" }); console.log(String(r));'
you said: cgo0

Same size to the byte-ish – 14 811 298 against 14 811 401 – so the pure-Go os/user and resolver cost nothing here.

Tier-2 container legs

Tier 2 (#1987) means: runs nightly and on pushes to main that touch pkgs/mcpx, never gates a PR. The legs are scripts/tier2-containers.sh, run by the mcpx tier-2 containers workflow; each leg asserts the surface its image can actually support, including mcpx’s own clean refusal where a surface is absent:

legimageasserts
alpine-nonexecalpine:3.21--version; exec refuses with “no usable JavaScript runtime”
alpine-bunmusl-execalpine + bun’s musl buildexec runs a script
scratch-nonexecFROM scratch + CA certs--version; the same clean exec refusal
distroless-static-nonexecgcr.io/distroless/static-debian12--version; the same clean exec refusal
distroless-cc-deno-execgcr.io/distroless/cc-debian12 + denoexec runs a script

Deferred, with the reason recorded on #1987: an alpine + nodejs exec leg. Verified 2026-10-30, mcpx’s node path dies with ERR_NO_TYPESCRIPT under Alpine’s nodejs build, so the leg would enshrine a failure instead of catching one; it joins the matrix when the node invocation works on a musl node. Tier 3 (fedora, flatpak, silverblue/kinoite hosts, the Windows port) is unchanged: separate issues when a user appears.

5. Where the size goes

Measured on linux/arm64 with the digests pinned in Containerfile, before and after §3, both built on the same machine:

layerwith gitwithout
debian:bookworm-slim108 MB108 MB
apt-get install105 MB (ca-certificates git)10.4 MB (ca-certificates)
bun79.4 MB79.4 MB
mcpx14.8 MB14.9 MB
layers, uncompressed307 MB213 MB
compressed content106 MB75.6 MB
docker images disk usage413 MB288 MB

The 413 MB this document used to quote is the last row, which is the uncompressed layers plus the compressed blobs they were unpacked from. The layers are what a running container occupies; the compressed size is what a pull transfers.

git costing more than bun was not expected: with --no-install-recommends already set, the weight is perl, which Debian’s git depends on outright. What remains of the apt layer is ca-certificates and the openssl it pulls in, for the registry. perl-base is still in the image – it is part of Debian’s essential set, in the base layer – but nothing in mcpx uses it.

6. What was deliberately left out

  • No per-PR container gate. The tier-2 legs (§4) run nightly and on pushes to main; building images on every PR is a cost the tier system deliberately does not pay.
  • No full e2e suite inside an image. The e2e suite spawns daemons and binds unix sockets; the tier-2 legs assert surfaces, which is a different, smaller harness.
  • No shipped scratch or distroless variant. They exist as tier-2 test legs; a shipped minimal image still drops exec (no static JS runtime exists to put in it), which is a product decision nobody has taken.
  • Not wired into package.nix. The Nix build and the container build are two ways to produce the same binary; making one depend on the other buys nothing yet.

7. What to watch

The .dockerignore excludes result – a dangling symlink into the Nix store that does not exist inside the build. If a nix build has been run in the tree and this file is removed, the container build will fail on a broken symlink, and the error will not say why.

Declared but never read

A knob that does nothing is worse than one that does not exist. A missing flag fails loudly at the prompt; a flag that parses, appears in --help, appears in mcpx config --schema and then changes nothing costs somebody an afternoon before they conclude the feature is broken rather than the wiring.

This has shipped repeatedly. --typecheck on exec (#66). Five exec flags at once (#75). MCP sampling, declared and unreachable (#39). The opencode plugin’s permission.ask hook. Every one of them was found by a person trying the thing, which is the expensive way.

This is the audit that went looking for the rest of them, and the guard that makes the next one a test failure.

What the audit was

The settings registry had 149 entries when this audit ran; it has 164 today, and the counts below are the ones the audit measured, not current figures. Each one derives a config key, an MCPX_ variable and a flag from a single declaration, which is the whole argument for the registry existing. The question is therefore not “is this path string mentioned anywhere” but “does anything read the resolved value”.

Those are not the same question, and the difference is where the interesting half of the findings came from. pool.max was read from a config file, through config.Config.Pool, which the JSON decoder fills. MCPX_POOL_MAX and --pool-max landed in the resolved Set, and nothing on that path looked at them. Grepping for "pool.max" finds a match in internal/defaults (an error label) and calls it live. A user who set the variable, saw nothing change, moved it into their config file and saw it work would reasonably conclude the variable name was wrong rather than that the layer was. That is fixed now: config.ApplyPoolSettings (internal/config/poolsettings.go:36) copies every layer of the resolved Set into Config.Pool, and a runtime change through settings set reloads the pools (internal/daemon/routes_settings.go:268), so the change reaches pools that are already running.

So the audit looked for one specific shape: the path handed to an accessor on a resolved Set — set.Int("pool.max"), a.Settings().Bool(...), cs.Duration(...). That is the only reader in the program that has seen the file, the variable and the flag together. os.Getenv has seen one layer. A config struct field has seen one. A hand-written flag variable has seen one.

It found 54, in a registry of 149.

Verdicts

Counts: 24 wired, 3 deleted, 9 legitimately external (the plugin — seven are named below, and two more were reclassified while the audit ran; there are twelve plugin-scoped settings today), 1 left open, 2 added (the inverse bug), plus 15 settings the naive path-string grep called dead that the accessor scan cleared.

Wired

settinglayerverdictevidencefix
pool.max pool.min pool.sharing pool.scope pool.idleTimeout pool.callTimeout pool.startTimeoutsettingswas partially dead — file only; wiredconfig.Config.Pool filled by the decoder (config.go:174); Resolve picks from it (config.go:616, the pick for max); no accessor read the resolved Set until ApplyPoolSettings (poolsettings.go:36)config.ApplyPoolSettings folds the resolved value into Config.Pool — the layer beneath a server’s own mcpx block, so a per-server choice still wins. Called at the five places a config is loaded next to a resolved set (internal/daemon/routes_settings.go:659, internal/cli/root.go:157, internal/cli/root.go:392, internal/cli/connect.go:216, internal/cli/doctor.go:97). A runtime change through settings set or settings unset reloads them (routes_settings.go:268), and a reload that fails is rolled back (routes_settings.go:280).
logging.maxBytes logging.maxLines logging.maxAge logging.keepsettingsdeadevery NewFileSink call passed FileOptions{Dir: dir} and nothing else (cli/root.go, cli/commands.go, cli/logs.go); file.go:87-96 fell back to the built-in constantscli.fileOptions builds the whole policy from the set; both durable sinks use it
logging.filesettingsdeadnothing decided whether to open a sink at allguards sink creation in the daemon and in run/exec
logging.includesettingspartially dead — flag and file onlyos.Getenv("MCPX_INCLUDE"), a name the registry does not own (root.go:157)reads set.List("logging.include")
script.before script.prefix script.onSuccess script.onError script.suffixsettingspartially dead — file and short flag onlycfg.ScriptPhase(...) plus a hand-written repeatable flag; --script-prefix and MCPX_SCRIPT_PREFIX were bound, recorded and never readApp.phaseValues adds Set.ListAboveFile(path). Only the layers above the file: cfg.ScriptPhase folds the file layers itself, with an inheritance marker the registry does not model, and taking both would run a line twice.
script.envsettingsdead twice over--env is a hand-written alias so Bind skipped it; and envPairs was assembled, preflight-checked and then ignored in favour of envVars.Values() at the point the environment was actually builtthe loop reads envPairs
script.launchersettingsdeadonly *launcherFlag was readfalls back to the resolved value; the Bare form resolves to none, so --script-launcher and --no-launcher agree
paths.state paths.cachesettingspartially dead — env alias onlydaemon.ResolvePaths reads MCPX_STATE_DIR/MCPX_CACHE_DIR by name (paths.go:37-42)daemon.PathsAt, applied in parseFlags once all three layers are known. The by-name reads stay, allowlisted: they run before any config file has been found.
output.jsonsettingspartially dead — global flag only--json consumed in main before the subcommand; mcpx ls --json set output.json and left App.JSON falseApp.adoptSettings in parseFlags
catalog.instructionssettingsdeadonly --no-instructions on types was readANDed with the setting; Commands narrowed to types, the only output that carries instructions
proto.askTimeout proto.askPoll proto.askRounds proto.stateTTL proto.sessionIdlesettingsdeadinternal/mcpserver read defaults.Proto* at the point of use (ask.go:155,165,196, state.go:59, server.go:1253)mcpserver.Timing, passed in from cli/serve.go. Passed rather than read there, because the protocol package should not know a config file exists.
proto.askTTLsettingsdeadroutes_proto.go:260s.set.Duration("proto.askTTL")
plumbing.sourceDirAllowed plumbing.sourceDirRecursive plumbing.sourceProbePathssettingsdead — and source.Options’ own doc comment claimed otherwiseboth call sites wrote the three booleans as literals, and disagreed: a launcher refused a directory while a phase accepted onecli.plumbingSourceOptions
plumbing.strictUnknownKeyssettingsdeadSet.Unknown() was reported by doctor as a warning and nothing elserefuses in App.Settings(), after the files that carry the switch have been read
plumbing.validatePathssettingsdeadthe one preflight path check ran unconditionally, on os.Getenv("MCPX_LOGGING_DIR")guarded by the switch, and reads the resolved logging.dir
plumbing.indexOnQuerysettingsdeadfour unconditional st.Ingest() callsguarded in cli.openStore and daemon.openStore. log --follow still ingests: its whole purpose is to see what was just written.
plumbing.launcherPlaceholderRepeatsettingsdeadopts.AllowRepeat came only from --allow-repeatappended from the setting
elicit.pendingLimitsettingsdeadelicit.go:415 used defaults.ElicitPendingpassed in as Filter.Limit; the broker is a store and should not read policy
registry.timeout registry.pageSizesettingsdeadregistry.go:43,129 used the constantsregistry.Options, passed by both callers
tasks.ttlsettingsdeadtasks.DefaultTTL() from the constantthe daemon supplies the configured value when a caller names none
plugin.discoveryRetryplugindeadTUNING.missCooldownMs: 60_000, hardcodedgoDurationMs(env.MCPX_PLUGIN_DISCOVERY_RETRY) — the plugin parses Go’s duration spelling, because that is the syntax every MCPX_ duration is written in
completion.maxValuessettingshalf-read/v1/complete read it (ops.go:350); the MCP handler wrote 100 (server.go:1558)Server.MaxCompletions

Deleted

settingwhy
plugin.skillsMCPX_PLUGIN_SKILLS appears in no TypeScript and no Go. Skills are directories a user copies into ~/.config/opencode/skills/ by hand; nothing registers them from a list, and there is no code for the setting to be wired into.
plumbing.consoleReleaseOnExit“hand the original console back before the process ends”. The console wrapper lives in the script’s runtime, which is a child process that is about to die. Restoring it changes nothing anybody can observe. releaseConsole() remains a global a script can call mid-run, which is the case that was real.
Setting.AllowDir (a schema field, not a setting)declared on six KindSource settings, always false, read by nothing. It also contradicted plumbing.sourceDirAllowed, which is the switch that actually decides this.

Legitimately external

plugin.bin, plugin.binArgs, plugin.backend, plugin.toolTiming, plugin.env, plugin.instructions, plugin.tools are read by the TypeScript plugin through process.env, by the exact name the registry derives (mcpx-session.ts:180-191, mcpx/daemon.ts:848-850). Verified, and now guarded: a plugin-scoped setting whose variable appears nowhere in plugin/**/*.ts fails the test. README mentions do not count — a README can describe a knob nobody wired.

Left open

paths.config was circular by construction (#5): the config search path is what produces the settings, so the settings cannot decide it. It is no longer left open. config.ResolveChainFrom resolves the chain in two passes (issue #1842): a discovery pass over the default locations, seeded by MCPX_PATHS_CONFIG / --paths-config, reads only paths.config; the load pass then resolves that list, nearest last. A paths.config found in pass 2 that differs is reported by mcpx config --sources, not applied, and a cycle is refused naming the path that closed it. MCPX_CONFIG bypasses both passes.

The inverse bug

The plugin honoured four variables the registry had never heard of, which breaks the registry’s promise from the other side: mcpx settings was supposed to answer “what will the plugin do” without anybody reading TypeScript. plugin.remember and plugin.annotate are now declared. MCPX_PLUGIN_HEADLESS and MCPX_PLUGIN_DAEMON_TOOLS were allowlisted at the time, because their defaults are computed at boot — whether the plugin is on the main thread, whether discovery was ambiguous — so a declared value would be right in one of the two realms the plugin runs in and wrong in the other. They are declared now, as plugin.headless and plugin.daemonTools (internal/settings/plugin.go:105, :114), with the default auto: auto is the declared name for “work it out from the process shape”, which is the thing a number could not express.

The guard

internal/settings/consumed_test.go, three tests.

TestEverySettingIsReadSomewhere scans every non-test .go file outside internal/settings for each path used as the argument to a Set accessor. A plugin-scoped setting is checked differently: its EnvName() must appear in the plugin’s own TypeScript sources.

The obvious alternative was a ReadBy: "daemon" field on Setting. It was rejected, and the reason is worth stating because it generalises: that note lives in the same struct literal as the declaration. Whoever writes the declaration writes the note in the same keystroke, believing both, and nothing ever checks the second half. It would have been a second declaration, subject to exactly the bug it was meant to catch. A scan asks the code, which cannot be optimistic.

The narrowness is the other half of the design. A scan for the path string anywhere would have passed pool.max, script.prefix, logging.include and every other half-wired setting in the table above — the ones that were hardest to find and most confusing to hit. Requiring the accessor is requiring the whole promise, because the Set is the only reader that has all three layers.

TestNoHandRolledSettingEnv closes the other direction: os.Getenv of a variable the registry owns is how a setting becomes env-only. Three entries on its allowlist, each because the read genuinely happens before a Set exists. (Since replaced by TestNoUndeclaredEnvRead, which walks the syntax tree and also refuses a variable nothing declares – see docs/environment.md.)

TestThePluginReadsNoUndeclaredSetting closes the third: a variable the plugin honours that no setting declares.

Every allowlist entry carries its reason in the map. There is deliberately no way to silence any of the three in bulk.

The other three layers

The same shape exists wherever something is declared in one file and consumed in another.

/v1 parameters

161 declared across 60 operations at the time of the audit; 162 across 60 today. The parity tests in internal/api already bind every route to an op and every op to an MCP tool — but they match paths, not parameters, so a parameter can be declared, published in InputSchema(), rendered into the OpenAPI document, marshalled into the body by a generated MCP tool, decoded by the handler and discarded, with every test green.

  • POST /v1/diagnose, session — decoded into the request struct and never referenced. Its own description said “for the record”, and no record was written. Fixed: a diagnose.run line now goes to the durable log, with the session on it, which is what makes it joinable to the run it was diagnosing.
  • GET /v1/log?chain= — Chain(id, limitPerLevel) takes no filters, so level, event, server, tool, session, trace, grep, since and until were all accepted and dropped. The result is the whole trace tree at every level, which reads as “that trace touched everything” rather than as a filter that did not run. Fixed: the combination is refused, in /v1 and in mcpx log. The e2e test for chains was itself passing --level debug.
  • GET /v1/stats?top= is inert for four of seven dimensions and GET /v1/events?since= loses to Last-Event-ID; both say so in their own descriptions. Not defects.

CLI flags

No flag declared with fs.String/fs.Bool in internal/cli is wholly unread. The defects are of two kinds.

The first is structural and was fixed: Schema.Bind registers every setting whose Commands list contains the command or is empty, so a command accepts flags it never reads. --budget and --bias were offered on types, ls and search; --catalog-instructions on ls and catalog. Narrowed.

The second is a class this audit did not resolve and is reported rather than fixed, because each case is a judgement about the command rather than a wiring error:

  • logging.* declares no Commands, so --log-level, --logging-format, --include, --keep and the rotation flags bind on every command. Only daemon and run/exec read them. mcpx log --log-level debug parses and does nothing. Narrowing logging.* would be correct and is a larger change than this one.
  • mcpx ls -v --json silently drops the per-instance detail -v asks for; the JSON branch returns before it is used. mcpx doctor -v --json likewise.
  • mcpx stats errors --by slowest reports errors: a positional dimension wins over --by through firstSet, silently.
  • mcpx stats --opencode returns before --server, --tool, --session and --log-dir are used.
  • mcpx log --format json --fields ts,msg ignores --format: --fields bypasses the writer entirely.
  • mcpx log --reverse --follow reverses the backlog and not the tail; followLog resets q.Reverse unconditionally.
  • mcpx prompt --mode script --run becomes run; mcpx recipes run x --mode run --script becomes script mode. Both overwrite the flag they were given.
  • mcpx servers add x --header K=V -- cmd accepts the headers and drops them: they are read only inside the --url branch.
  • mcpx api tools --methods all does nothing without --spec: six flags are read only inside if *specFlag != "".
  • mcpx elicit show --session S accepts --session and --audience on every subcommand and reads them in two.
  • parseFlags leaks into the package-level flagSetCommand map when fs.Parse fails, and the map is unguarded while lazyMCP builds on a request goroutine.

MCP capabilities

  • logging — declared to every pre-2026-07-28 client. The capability means “this server sends log messages to the client”; mcpx emits no notifications/message anywhere in the package. Withdrawn. The logging/setLevel method is still answered, because refusing would make a well-behaved client that asked anyway treat the whole connection as degraded.
  • resources.subscribe, *.listChanged — gated on s.Notify != nil, a property of the server. Delivery is a property of the connection, and every HTTP connection the daemon serves is built with a nil send function. So every client over the transport the daemon actually mounts was told to subscribe, had its subscriptions/listen acknowledged, and then heard nothing for ever. Now gated on Conn.canPush().
  • *.listChanged additionally requires a stream the client opened, and only 2026-07-28 has subscriptions/listen. An older client was told it would receive notifications it has no mechanism to receive. Now gated on the revision too.
  • subscriptionId in _meta — 2026-07-28 makes it mandatory on every notification delivered on a listen stream (2026-07-28.ts:121-132). mcpx sets it on the acknowledgement only. A client with two subscriptions cannot correlate anything. Since fixed: every notification on a listen stream carries it (internal/mcpserver/listen.go:94), pinned by internal/e2e/subscribe_test.go and internal/e2e/protomsg_test.go.
  • prompts and resources are declared unconditionally; both schemas say “present if the server offers any”. The handlers return [] correctly, so this is over-declaration rather than a lie. Not fixed — reported.
  • events.MCPNotification translates ElicitCompleted to notifications/elicitation/complete, and nothing can reach that branch: ListenFilter has no field for it, correctly, since 2026-07-28 dropped it.

One thing the audit found that it did not fix

Setting.EnvName() derived MCPX_PROTO_ASK_T_T_L from proto.askTTL, and --proto-ask-t-t-l with it. The camel-case splitter did not know that TTL is one word. proto.stateTTL, proto.askTTL, daemon.leaseTTL and proto.serveMCP were affected.

Fixed since: a run of capitals is now one word (MCPX_PROTO_ASK_TTL, --proto-serve-mcp), and TestNoDeclaredNameHasASingleLetterWord fails if a derived flag or variable contains a one-letter word, which is what the next acronym split into letters would look like.

Dependencies

Everything mcpx needs, and why. Nothing here is incidental — if it is listed, something breaks without it.

Go libraries

Nine, and pkgs/mcpx/package.nix states the trade for each one beside the vendorHash that pays for them.

modulewhat it is for
modernc.org/sqlitethe log index
github.com/charmbracelet/bubbleteathe full-screen browser, mcpx tui
github.com/charmbracelet/bubblesits widgets
github.com/charmbracelet/lipglossits layout
gopkg.in/yaml.v3OpenAPI documents, which are published as YAML as often as JSON, and which one you get is not the caller’s choice
github.com/santhosh-tekuri/jsonschema/v6the JSON Schema validator (internal/schemaval): tool input/output schemas and elicitation schemas are checked with a real validator, not by hand
github.com/tetratelabs/wazerothe WebAssembly runtime: wasm tooling and the embeddings worker run without a cgo toolchain
golang.org/x/systhe OS-specific syscalls the standard library does not expose (peer credentials on the daemon socket, process groups)
tailscale.comthe tsnet identity transport (#1967): the daemon embeds a Tailscale node and is reachable by its tailnet identity, still as a pure-Go CGO_ENABLED=0 binary

The three charmbracelet modules are one decision, not three: they cost about 1.2 MB in the stripped binary and replace raw terminal handling – escape sequences, resize, a redraw loop – which is the kind of code that is never finished and never correct on every terminal.

Everything else – the MCP client, the JSON-RPC framing, the process pools, the JSON Schema to TypeScript compiler, the HTTP API and the CLI – is standard library, and that is worth keeping.

Why this driver

modernc.org/sqlite is a pure-Go translation of SQLite rather than a binding. The usual choice, mattn/go-sqlite3, is cgo, and cgo would mean:

  • a C toolchain required to build, which the Nix derivation would have to carry;
  • cross-compilation broken, so no building a Linux binary from a Mac;
  • CGO_ENABLED=0 builds failing outright.

That is a large bill for a database that is only ever an index. The JSONL files remain the source of truth: the index can be deleted at any time and is rebuilt on the next query. Paying in build complexity for something reconstructible was the wrong trade.

The cost of the pure-Go driver is a slower build and a larger binary. Both are acceptable; neither affects anyone running mcpx.

What this means for the build

  • vendorHash is pinned in pkgs/mcpx/package.nix and must be updated when the dependency changes.
  • The build still needs no network beyond the vendor fetch, and still needs no C toolchain.

The standard library packages doing real work: net/http (daemon and client), encoding/json (everything), os/exec + syscall (child processes and process groups), sync (pools), path/filepath (path identity), text/tabwriter (human output).

If a dependency is ever added, it should be for something genuinely hard — a tokeniser, a SQLite driver — and the trade should be stated here.

External programs

ProgramNeeded forIf missing
gitrepo and worktree scopes, for repository layouts native discovery does not readonly those layouts degrade to cwd, with a log line and a doctor warning saying why
deno, bun or nodemcpx run and mcpx execthose commands fail; mcpx call, ls, types, catalog, search still work
the configured MCP serverstheir own namespacesthat namespace reports error with the server’s stderr

git is optional. mcpx finds a repository by reading .git itself – a port of git’s discovery rules, docs/git-discovery.md – and runs git rev-parse --path-format=absolute --git-common-dir --show-toplevel only for a layout that port does not vouch for, such as a repository format newer than it knows. That needs git 2.31 (March 2021); an older git’s answer is refused rather than misread. Nothing else about git is used — no fetching, no writing, no config.

A JavaScript runtime is chosen at first use: deno, then bun, then node. Override with --runtime or the runtime config key. Deno runs with --no-check; the generated client is machine-written and a type error in the agent’s own script surfaces at runtime anyway.

MCP servers are whatever the config names. mcpx spawns them with the given command, args, env and cwd, or connects over Streamable HTTP for url servers. Their own configuration is exactly those argv and environment entries — MCP has no separate configuration channel.

Network

The daemon binds two listeners, both local: a unix socket, and 127.0.0.1 on an OS-assigned ephemeral port. Generated script clients prefer the socket — Deno fetches over it with a custom HTTP client and Bun with a fetch option (internal/codegen/emit.go) — and fall back to the TCP port where a runtime cannot address a socket (Node’s fetch cannot, without undici). Neither listener is reachable off the machine; the tailnet identity transport (#1967) is the separate, authenticated exception, and it is opt-in.

Outbound traffic happens only when a configured server is a url server, or when a script makes its own requests.

Nix packaging

pkgs/mcpx/package.nix wraps the binary with git, deno, bun-bin and nodejs suffixed onto PATH, so behaviour does not depend on the calling shell. git is in that list because repo and worktree scopes are silently weaker without it — that was a real gap, found by asking this question.

The unit suites run in the build sandbox. The end-to-end suite spawns JavaScript runtimes and binds unix sockets, so it runs outside with go test ./....

Elicitation: a design

id:           elicitation-a-design
since:        pre-release
created:      2026-09-28T18:00:00-05:00
last-updated: 2026-09-28T18:00:00-05:00
status:       partial
tags:         area:protocol, area:integration
description:  how a server asks a question, and how the answer gets back
              through a CLI, a REST call, a script, an MCP host or a plugin.
note:         largely shipped, and differently in places

This was written before the feature existed, and the feature has since shipped – not always in the shape proposed here. It is kept as the design record, because the reasoning is still the reasoning. What actually ships is in docs/parity.md (the elicit_* rows) and docs/consumer.md; the authority is internal/elicit/, internal/cli/elicit.go and the three /v1/elicit routes in internal/daemon/server.go:424-426.

Where this document and the code disagree, the code is right. The disagreements worth knowing before you read on:

this document sayswhat shipped
§2’s three “live bugs”all three fixed: dispatch routes server-initiated requests (internal/mcpclient/client.go:404-421), negotiate no longer echoes a version it does not serve (internal/mcpserver/server.go:1092-1095), and the client declares elicitation (internal/mcpclient/modern.go:71-84)
mcpx call --elicit=<mode>not built. There is no --elicit flag on any command
a waiting call exits 75built (#286), with a different document: mcpx call, mcpx exec and mcpx run exit 75 with the question, its schema, the mcpx elicit answer command and the task id that collects the result. The call keeps running on the daemon as a task. The wire is POST /v1/call with X-Mcpx-Input: report, answered 202 {"inputRequired": {...}} – see §5.1
POST /v1/elicit/<id>/answerPOST /v1/elicit/{id}/{action} – the action is in the path, the body is content only
GET /v1/elicit/subscribenot built. mcpx elicit watch polls; the SSE stream is GET /v1/events
mcpx_elicit_pendingthe tool is mcpx_elicit_list
plumbing.elicitTTL, elicit.mode, elicit.rulesnone exist. The TTL is elicit.ttl in defaults.json and is not a setting; the shipped policy is elicit.Route (internal/elicit/elicit.go:491), which is not rule-based
MCPX_PLUGIN_ELICITdoes not exist
onElicit() / ask() script globals, the TUI view, the mcpx-elicitation skillnone built
a 202 with Location and Retry-After, an Elicitation OpenAPI componentnot built. The retry shape that shipped is MCP-side input_required

1. What elicitation actually is

Every other MCP method runs client → server. Elicitation runs the other way: a server asks the client a question in the middle of handling a tool call, and waits for an answer before finishing.

mcpx ──── tools/call ────────────────▶ server
mcpx ◀─── elicitation/create ──────── server     "which account?"
mcpx ──── {action:"accept", ...} ────▶ server
mcpx ◀─── the tool result ──────────── server

Two modes:

modecarriesfor
formrequestedSchema, a flat object of primitivesa value the caller can type
urlurl + elicitationIdanything that must not pass through a model: credentials, OAuth, payment

Three answers, and the distinction matters:

  • accept — approved, with content matching the schema
  • decline — explicitly refused. The server should offer an alternative.
  • cancel — dismissed without choosing. The server may ask again later.

Collapsing decline and cancel is the obvious mistake. A server that treats “not now” as “never” stops offering something the user wanted.

Two eras, and mcpx is in the older one

This is bigger than elicitation, and correcting it is a prerequisite.

The spec divides implementations into two eras:

versionshow it works
legacy2025-11-25 and earlierinitialize handshake establishes a session; version negotiated once
modern2026-07-28 (current) and laterno handshake; every request carries its version in _meta; server/discover is mandatory

The current version is 2026-07-28. It is not a draft.

The compatibility matrix is unforgiving. A modern client against a legacy server fails. A legacy client against a modern server fails. Only a dual-era implementation bridges them, and it must implement both.

mcpx was legacy on both sides when this was written: its client sent initialize with ProtocolVersion = "2025-06-18", and its server answered initialize. It is dual-era now – the client offers 2025-11-25 (internal/mcpclient/client.go:22) and probes modern with server/discover first, and the server serves both eras.

And its server lies. negotiate() echoes whatever version the client asked for:

$ printf '{"jsonrpc":"2.0","id":1,"method":"initialize",
           "params":{"protocolVersion":"2026-07-28"}}\n' | mcpx serve
  we claimed: 2026-07-28
$ ... server/discover
  -32601 no method server/discover

A modern client is told “yes, I speak 2026-07-28”, and then finds no server/discover and no _meta handling. The spec says an unsupported version MUST get an UnsupportedProtocolVersionError (-32022) listing what is actually supported. This was a second live bug, independent of elicitation. It is fixed: negotiate echoes a version only when it is both supported and non-modern, and otherwise answers Latest (internal/mcpserver/server.go:1092-1095); server/discover is implemented (server.go:535) and -32022 exists (server.go:327).

Elicitation in each era

The delivery mechanism differs, which is why the era question comes first.

  • Legacy: a genuine mid-flight request. The server sends elicitation/create while tools/call is still open, and waits.
  • Modern: a retry. The call returns an InputRequiredResult carrying inputRequests, the call ends, and the client re-sends the original request with inputResponses attached.

The modern shape is dramatically easier for mcpx, because nothing stays open: an invocation can return and a person can answer an hour later. The legacy shape is what every server that exists today actually speaks.

So the broker below is designed around the modern semantics – a question is state with a deadline – and the legacy path is implemented by holding the call open and feeding it from the same broker. Both are needed. One shape of storage, two shapes of wire.


2. What is in the way today

recvLoop silently drops server-initiated requests

internal/mcpclient/client.go:

var resp rpcResponse
if err := json.Unmarshal(raw, &resp); err != nil { continue }
if resp.ID == nil { continue }       // notification
ch, ok := c.pending[*resp.ID]        // ← a request has an ID too
if ok { ch <- &resp }                // ← no match, frame discarded

A server→client request has both an id and a method. It unmarshals cleanly, finds no pending entry, and is dropped. The server then waits forever, the call hits pool.callTimeout, and the error says “timed out” — which is true and useless.

This was a live bug, not only a missing feature, and it has been fixed: recvLoop delegates to dispatch, which looks for a method as well as an id and routes the frame to handleServerRequest (internal/mcpclient/client.go:404-421).

initialize does not declare the capability

It did not, when this was written. mcpx advertised tools, resources, prompts, and a well-behaved server checking for elicitation would not send one – protection by silence, which is luck rather than design. The client now declares elicitation (with form, and url when a handler is set), roots and sampling (internal/mcpclient/modern.go:71-84, wired in at client.go:315).

Nothing owns “a question awaiting an answer”

Every existing mcpx state is either in-process (a lease) or durable and append-only (the log). A pending elicitation is neither: it outlives a process and it is mutated once.


3. The shape: a broker

One new component. internal/elicit.

type Request struct {
    ID        string            // elc-<16 hex>
    Trace     string            // the call that triggered it
    Parent    string            // that call's own parent
    Session   string            // who owns it
    Server    string            // which server is asking
    Tool      string            // during which tool call
    Mode      string            // form | url
    Message   string            // why, in words
    Schema    json.RawMessage   // form mode
    URL       string            // url mode
    Created   time.Time
    ExpiresAt time.Time         // Created + TTL
    State     string            // pending | answered | expired | abandoned
}

type Answer struct {
    ID      string
    Action  string            // accept | decline | cancel
    Content map[string]any    // accept, form mode only
    By      string            // which surface answered
    At      time.Time
}

Stored in the existing SQLite index beside records, in two tables. It is already there, already rotated, already queryable, and a pending question is exactly the kind of thing somebody will later want to ask mcpx log sql about.

The broker is one object with four operations:

Proposed as four operations; what shipped is seven, in internal/elicit/elicit.go:

OpenRequest(r Request) (Request, error)        // register; fills id, mode, TTL
Await(ctx, id) (Answer, error)                 // block until answered or expired
Respond(a Answer) error                        // from any surface
Get(id) (Request, bool, error)                 // the question
Lookup(id) (Answer, bool, error)               // its answer, once it has one
Pending(f Filter) ([]Request, error)           // what is outstanding
Close() error

Await takes no timeout: the deadline rides on the Request, so one question cannot be awaited with two different lifetimes.

Await blocks on a channel when the answerer is in-process, and polls the table when it is not. Polling is acceptable here because the interval that matters is human — a 250 ms poll against a question a person answers in twenty seconds is not a cost anyone can measure.

Time to live

Every request carries one. The default is elicit.ttl, 120 s, in internal/defaults/defaults.json (defaults.ElicitTTL). It is not a registry setting and not per-server overridable: OpenRequest (internal/elicit/elicit.go:206) and the daemon’s hooks use the constant unconditionally. Making it a setting is unfinished work, not a decision.

On expiry the broker answers cancel on the requester’s behalf, because that is what expiry means: dismissed without an explicit choice. Answering decline would tell the server the user said no, which is a different and wrong thing.

The TTL is also the backpressure. Without it a stuck server accumulates questions nobody will ever see, and the table becomes a graveyard.


4. The policy layer: answering without a human

A CLI invocation frequently has no human attached. Guessing is unacceptable; failing every time is unusable. So the broker consults a policy first, and only asks a human if the policy abstains.

Not built. None of elicit.mode, elicit.ttl as a setting, or elicit.rules is in the registry, and MCPX_ASSUME_YES does not exist. The policy that shipped is elicit.Route (internal/elicit/elicit.go:491), which chooses an audience rather than matching rules, together with the settings elicit.disambiguate, elicit.disambiguateDefault, elicit.confirmDestructive and elicit.askTimeout. The block below is the design, kept for the reasoning.

{
  "elicit": {
    "mode": "ask", // ask | auto | decline | error
    "ttl": "120s",
    "rules": [
      { "match": { "server": "github", "field": "username" }, "answer": "octocat" },
      { "match": { "mode": "url" }, "action": "decline", "because": "no browser in CI" },
      { "match": { "field": "confirm" }, "action": "accept", "when": "MCPX_ASSUME_YES=1" },
    ],
  },
}

The four modes:

  • ask (default, interactive) — a human is expected
  • auto — rules only; anything unmatched is cancel. This is the CI mode.
  • decline — refuse everything, politely. For a sandbox that should never be asked.
  • error — fail the call outright, so a script that did not expect a question does not silently get a cancelled one

Rules are matched before a human is ever asked, and every match is logged with the rule that fired. A policy nobody can audit is a policy nobody should trust.


5. Every surface

The same broker, six front doors. The rule throughout: an unanswered question is a first-class result, not an error.

5.1 CLI — the exchange you described

A call that elicits does not hang. It returns a document and a non-zero exit code reserved for this case (75, EX_TEMPFAIL, which is what it is).

$ mcpx call github.create_issue '{"title":"x"}'
{
  "status": "input-required",
  "elicitation": {
    "id": "elc-9f2c1a84bb0e7d31",
    "trace": "cal-d13e4c64102e9113",
    "session": "s87630-989154000",
    "server": "github",
    "mode": "form",
    "message": "Which repository should this issue go in?",
    "requestedSchema": {
      "type": "object",
      "properties": { "repo": { "type": "string", "description": "owner/name" } },
      "required": ["repo"]
    },
    "expiresAt": "2026-09-28T18:02:00Z",
    "ttlSeconds": 120,
    "respondWith": "mcpx elicit answer elc-9f2c1a84bb0e7d31 '{\"repo\":\"me/thing\"}'"
  }
}
$ echo $?
75

What shipped (#286) carries the same information in a different shape:

$ mcpx call ask.need_repo; echo $?
mcpx: ask.need_repo is waiting for input and nothing here can answer it.

  asks     Which repository should this go in?
  schema   {"properties":{"repo":{"description":"owner/name","type":"string"}},"required":["repo"],"type":"object"}
  expires  2026-09-30T22:31:37-05:00 (2m0s from now)
  answer   mcpx elicit answer elc-93e18b28a5a3c1f5 '{"repo":"..."}'
           mcpx elicit decline elc-93e18b28a5a3c1f5 | mcpx elicit cancel elc-93e18b28a5a3c1f5

The call keeps running until it is answered or expires; collect its result with: mcpx task result tsk-06654ccc9f064a5c
75
$ mcpx elicit answer elc-93e18b28a5a3c1f5 '{"repo":"me/thing"}'
$ mcpx task result tsk-06654ccc9f064a5c      # the call's real result

--json puts {"inputRequired": {callId, server, tool, questions[], text}} on stdout. A script under mcpx exec or mcpx run stops at the call that asked and exits 75 with the same message, even if it catches ToolError: the generated client sees the daemon’s 202 and exits rather than throwing.

Two limits. A daemon running inside the command (daemon.inline) stops with it, so the call cannot outlive the exit and the message says so. And when other calls share the server’s instance a question cannot be tied to one call for certain; it is still reported, marked ambiguous, because waiting out the TTL and exiting 0 was the bug.

respondWith is there because the alternative is the caller assembling it from three fields, and they will get it wrong once and then copy it forever.

$ mcpx elicit answer elc-9f2c1a84bb0e7d31 '{"repo":"me/thing"}'
$ mcpx elicit decline elc-9f2c...      # explicitly no
$ mcpx elicit cancel  elc-9f2c...      # dismissed
$ mcpx elicit list                     # what is outstanding, with time left
$ mcpx elicit watch                    # follow, for a terminal left open

Under the retry model, answering re-drives the original call automatically and the result lands in the log; mcpx elicit result <id> fetches it. Under the bidirectional model the original invocation is still blocked and simply proceeds.

A flag for the common case, where the caller is a person who will wait. Not built – mcpx call takes only -session and -raw, and --elicit=prompt is rejected as an undefined flag:

$ mcpx call --elicit=prompt github.create_issue '{...}'   # NOT BUILT
Which repository should this issue go in?
  repo (owner/name): me/thing

--elicit takes prompt | return | auto | decline | error, defaulting to prompt on a terminal and return otherwise. That default is the whole ergonomic story: interactive use just works, scripted use gets a document.

5.2 Scripts

A script is the one place where blocking is natural, because a script already awaits.

const issue = await tools.github.create_issue({ title: "x" });

By default the runtime answers from policy or cancels, and the call returns whatever the server does with that. To participate – not built: no onElicit and no ask are emitted into the script’s globals (internal/codegen/emit.go):

onElicit(async (q) => {
  // NOT BUILT
  if (q.mode === "url") return { action: "decline" };
  if (q.schema.properties.repo) return { action: "accept", content: { repo: "me/thing" } };
  return { action: "cancel" };
});

And for a script run on a terminal, ask(q) would prompt. The handler is registered rather than passed per call, because the question arrives from inside a call the script did not know would ask.

5.3 REST and OpenAPI

HTTP has a status code for exactly this, and it is not 400.

POST /v1/call/github/create_issue
→ 202 Accepted          # built only with X-Mcpx-Input: report, as {"inputRequired": {...}}; no Location/Retry-After
  Location: /v1/elicit/elc-9f2c1a84bb0e7d31
  Retry-After: 120

  { "status": "input-required", "elicitation": { ... } }
POST /v1/elicit/elc-9f2c.../accept   {"content":{...}}   # also /decline, /cancel
GET  /v1/elicit                       list
GET  /v1/elicit/elc-9f2c...           one, with time remaining

202 is right: the request was understood and accepted, and completion is pending. A 400 would say the caller made a mistake, which they did not.

The OpenAPI document gains a 202 response on every tool path and an Elicitation schema component. Both are generated, so they cannot drift. Neither is built: no operation declares a 202, and there is no Elicitation component in internal/api/openapi.go. What shipped instead is the MCP-side input_required result (internal/mcpserver/ask.go).

5.4 mcpx as an MCP server — the pass-through

This is the interesting one, and the reason the whole design is worth building. A question from a server three layers down has to reach the agent at the top.

agent ──▶ mcpx (MCP server) ──▶ mcpx daemon ──▶ github server
                                                      │
agent ◀── elicitation/create ◀── broker ◀────── "which repo?"

mcpx declares elicitation in its own initialize capabilities only if its client did. Capability negotiation has to be honest in both directions, or mcpx promises something it cannot deliver and the failure surfaces at the worst moment.

Then:

  • Client supports elicitation → forward it, rewriting the message to name the real originator: “github (via mcpx) asks: which repository?”. The agent must know which server is asking; that is a spec requirement and also just decent.
  • Client does not → fall back to policy, then to returning input-required in the tool result text so the agent can call mcpx_elicit_answer itself.

That fallback means an agent can answer a question with no client support at all, which matters because most MCP hosts do not implement elicitation yet.

Two new tools on the MCP surface:

  • mcpx_elicit_list — what is waiting, with schemas and deadlines
  • mcpx_elicit_answer — answer one

5.4a Who is the question for: the agent or the person?

The spec does not have a field for this, and deliberately so. From the Python SDK’s own words: “If the client is an agent, it might decide how to handle the elicitation – either by asking the user or automatically generating a response.” The decision is the client’s, not the server’s.

So mcpx needs a routing policy, and the useful thing is that the elicitation itself carries enough to route on:

signalroute towhy
mode: "url"humanthe point is a browser and a person’s consent
format: "password", or a name matching token|secret|key|passwordhumana model should not be handed a credential, and cannot know one
a confirm/approve boolean on a destructive callhumanconsent is the whole content of the question
an enum the agent can evaluate from contextagent“which of these three repos” is answerable from what it just read
free text with no constraintagent first, human on declinethe agent may know; if it does not it should decline, not invent

The last row is the one that matters. A model asked “which repository?” with no way to know will produce a plausible name. The default for anything the agent cannot verify must be decline, not guess, and the skill in §5.7 exists mostly to say so.

A server may hint through _meta – io.mcpx/audience: "user" – and mcpx should honour a hint toward the human and ignore one toward the agent, because a server pushing a question past a person is exactly the direction that needs resisting.

In the plugin this becomes concrete: notify puts it to the agent, ask puts it to the person, and the routing policy decides which when the mode is auto. A question routed to the agent that the agent declines is re-raised to the person rather than failing – that escalation is the behaviour somebody would expect and would otherwise have to build by hand.

5.5 The opencode plugin

The plugin is where this becomes genuinely good, because opencode has a real user and a real UI.

"tool.execute.after": async (input, output) => {
  const pending = await pendingFor(input.sessionID)
  if (!pending.length) return
  output.output += "\n\n" + renderElicitation(pending[0])
}

Three levels, opt-in as everything else in that plugin is. Not built: MCPX_PLUGIN_ELICIT is not a variable the registry derives or the plugin reads, and there is no --plugin-elicit.

  1. MCPX_PLUGIN_ELICIT=notify — a pending question is appended to the tool result, so the agent sees it and can answer via a tool. No UI needed.
  2. MCPX_PLUGIN_ELICIT=ask — the plugin uses opencode’s own permission mechanism (ask({ permission, metadata })) to put the question to the user. This is the real prize: a genuine dialog, in the harness the person is already looking at.
  3. MCPX_PLUGIN_ELICIT=auto — policy only.

The session id the plugin already injects is what makes this work. A question raised by a call from session X is offered to session X and to nobody else. No new plumbing; the existing injection is exactly the binding the spec requires between an elicitation and a user identity.

5.6 TUI

Not built – nothing in internal/tui/ mentions elicitation.

A seventh view, and a badge in the header when something is pending — because the one thing worse than a question nobody answers is a question nobody knows was asked.

Form mode renders the schema as fields. URL mode shows the URL and offers to open it. a accepts, d declines, c cancels, enter opens the detail.

The header badge is deliberately loud: ● 1 question in the accent colour.

5.7 Skills

Not built – there is no mcpx-elicitation skill in plugin/opencode/skills/.

mcpx-elicitation, loaded on demand, would cover: what input-required means, that decline and cancel are different, how to answer from a tool, and the one thing an agent gets wrong — inventing a value rather than declining. A model asked “which repository?” with no way to know will happily guess. The skill’s job is to say: if you do not know, decline and explain why.


6. What it unlocks

Elicitation is not only a feature. It is the missing half of four things mcpx already has.

Third-party authorisation flows

I overstated this in the first draft. Two different things share the word OAuth:

  1. MCP’s own authorization spec – how a client authenticates to a remote MCP server over HTTP. That is a separate document, separate machinery (OAuth 2.1, resource metadata, token handling), and elicitation does not provide it. It remains a genuine gap.
  2. A server sending its user through someone else’s authorisation flow – “connect your GitHub account”. That is url-mode elicitation, and it does fall out of this design.

The second is common and currently impossible. The first is unaffected. The line in OPENCODE-V2.md should be split accordingly rather than crossed off.

Write operations in OpenAPI and CLI adapters

Both currently default to read-only, because a specification describes what a service can do rather than what you meant to allow. With elicitation there is a middle setting: expose writes, and elicit a confirmation before each one.

{ "methods": ["all"], "confirm": ["POST", "DELETE"] }

That is strictly better than the two options available now, which are “no writes” and “all writes, no questions asked”.

Credentials at the moment of need

mcpx registry add currently reports which variables a server requires and leaves them empty, because inventing a secret is wrong. With elicitation the server can ask for it on first use, and the answer can go to a keychain rather than a config file. The current behaviour is the best available without elicitation; this is better.

Disambiguation instead of guessing

“Three repositories match, which did you mean?” is the single most common thing a tool cannot currently do. Today it either guesses or fails. Neither is what a person would do.

And two things it unlocks that are not about servers at all

mcpx can elicit on its own behalf. mcpx doctor finding an unconfigured server could ask rather than report. mcpx registry add could ask for the secrets it currently lists. The broker does not care whether the asker is an upstream server or mcpx itself, and building it only for pass-through would leave that on the table.

Long-running approvals become possible. With a TTL measured in minutes and a durable store, “this deploy needs a second pair of eyes” is expressible: a question raised by CI, answered by a person from a terminal or the TUI, and the call resumes. That is a different product feature that falls out of the same machinery.


6a. How the answer travels: four mechanisms, not one

The re-exec shape in §5.1 is right for one case and wrong as the only option. There are four ways an answer can reach the broker, and each is best somewhere.

mechanismlatencysurvives exitbest for
stdin/stdout promptimmediatenoa person at a terminal
re-exec (mcpx elicit answer)seconds to hoursyesscripts, CI, anything that returned
daemon socket subscriptionimmediatewhile connectedthe TUI, the plugin, any long-lived client
HTTP (POST /v1/elicit/<id>/<action>)immediateyesremote callers, webhooks, another machine

The daemon already owns a unix socket and a loopback port. A long-lived client – the TUI, the plugin, a mcpx elicit watch left running – should subscribe rather than poll:

GET /v1/events                    → server-sent events, carrying elicit.* kinds
                                    (GET /v1/elicit/subscribe was proposed and not built;
                                     mcpx elicit watch polls, internal/cli/elicit.go:164)

That removes the polling in §3 for every case that matters, and polling survives only as the fallback for a process that cannot hold a connection.

The defaults follow from the table:

  • terminal → prompt, no re-exec, no state to reconcile
  • non-terminal → return the document, expect mcpx elicit answer
  • TUI, plugin → subscribe
  • REST caller → the HTTP endpoints, which they are already speaking

Re-exec is the conventional shape because it is the only one that works when the asking process is gone, and that is the case a CLI has to handle. It is not the best shape when the process is still there.

6b. Continuity across the re-exec

A re-exec means a new process, and the question is whether anything is lost.

Nothing needs to be, because session identity already lives in the environment rather than the process. MCPX_SESSION_ID is injected by the harness; a second invocation in the same shell carries the same value and therefore leases the same pooled server.

What the audit trail needs, and what the design should record:

elicit.open      trace=cal-d13e… elicit=elc-9f2c… session=s876… pid=41201
elicit.answered  elicit=elc-9f2c… action=accept by=cli
                 session=s876… pid=41533 answeredAfterMs=18420
elicit.resumed   trace=cal-a77b… parent=cal-d13e… elicit=elc-9f2c…

Three things fall out of that. The pid changes and is recorded on both sides, so “who answered this” is answerable. The session is the same, so mcpx log --chain walks from the resumed call back through the elicitation to the original call. And answeredAfterMs is the metric worth having: it is the human latency, and it is the number that tells you whether a TTL is set sensibly.

If the session id is absent – a bare shell, no harness – the broker binds the question to the config fingerprint and the working directory instead, and says so in the record. Refusing to work without a session would make the feature unusable from a plain terminal, which is where people debug.

7. Cost, and what could go wrong

Roughly 1,200 lines, in five pieces. The broker and its two tables. The recvLoop fix plus a bidirectional path. Policy evaluation. Six surface adaptations, most of them under fifty lines each. The TUI view.

Three things I would expect to go wrong:

Deadlock under the bidirectional model. A call holds a lease while waiting for an answer; the answer arrives through a surface that needs the same pool. Mitigated by the TTL, but the retry model avoids it entirely — which is the strongest argument for preferring it.

Questions nobody sees. A CLI invocation that returns input-required into a script that ignores exit codes produces a question that expires unanswered, and the user sees only a mysterious cancellation. The TUI badge and mcpx elicit list are the mitigation; so would be a --elicit=error, which would make not expecting a question a loud failure rather than a quiet one. Neither the badge nor the flag is built.

Rule-based auto-answers drifting into dishonesty. A rule that answers “yes” to everything is a policy that lies to a server about a user’s consent. Logging every rule that fires is the minimum; I would also make auto mode refuse to match a rule whose match is empty, so there is no “match everything” shorthand.


8. The explainer

Everything mcpx does today assumes the conversation goes one way. You ask a server to do something; it does it; you read the answer. Every part of mcpx — the CLI, the scripts, the REST endpoints, the MCP server, the TUI — is shaped by that assumption.

Elicitation breaks it. It lets a server stop halfway through a job and ask a question: which repository did you mean? are you sure you want to delete this? I need you to log in — here is the link. The server cannot continue until somebody answers.

That is awkward for mcpx specifically, because mcpx is usually invoked by something that cannot answer. A shell script has no opinion about which repository. A cron job cannot log in. A model can answer, but it will happily invent a repository name it has no way to know, which is worse than not answering at all.

So the design turns a blocking question into a piece of state that anything can answer, later. The question gets an identity, a deadline and a home in the existing database. The tool call stops and says “I need input, here is what and here is the ticket”. Anyone holding that ticket — a person at a terminal, a script that knew this might happen, an agent through a tool, a dialog in the editor — can supply the answer, and the call resumes.

Three consequences are worth stating plainly.

The answer does not have to come from whoever asked. CI can raise a question a human answers from a laptop twenty minutes later. That is not a special feature; it falls out of the question being state rather than a blocked function call.

“No” and “not now” stay different. The spec separates decline from cancel, and it matters: a server told “no” should offer an alternative, and a server told “not now” should ask again later. Collapsing them — which is the tempting simplification — makes tools worse in a way nobody traces back to this decision.

Not answering is a legitimate outcome. A deadline expires, the question becomes a cancellation, the server is told honestly that nobody chose. The failure mode to avoid is a system that waits forever, or one that invents an answer to avoid waiting.

The reason to build it is not the feature itself. It is that four things mcpx already does are currently half-finished for want of a way to ask a question. OAuth is elicitation. Safe write access is elicitation plus a confirmation. Credentials-on-first-use is elicitation. Disambiguation is elicitation. Each of those is presently either missing or solved by refusing to do the risky thing, and they all become available from one piece of machinery.

The catch worth knowing when this was written – since fixed: mcpx’s receive loop treated every inbound frame as a reply to something it sent. A server that asks a question gets silence, waits, and eventually times out — and the error mcpx reports is “the call timed out”, which is true, useless, and points at entirely the wrong thing. That is worth fixing whether or not the rest of this is ever built.

The script environment

Settings are what a person tells mcpx. This page is the other direction: what mcpx, and the opencode plugin, tell a program they start.

A script reads its session from here. The generated client finds the daemon from here – the endpoint, the socket, whether the daemon can see this filesystem. An mcpx command run inside a script, or inside a shell the editor opened, learns which session it belongs to from here, which is what lets a session-scoped server give each agent its own browser instead of sharing one. It is an interface with as many consumers as the settings have.

Until #172 it was declared nowhere. The only list was the code that happened to set each name, spread across five packages and a TypeScript file, and the documentation had already drifted from it: it said the plugin sets MCPX_SESSION and MCPX_CALL. The plugin sets MCPX_SESSION_ID and MCPX_CALL_ID; MCPX_SESSION is a different variable, set by mcpx for its own scripts; and MCPX_CALL has never existed. Two variables, one renamed in somebody’s head, is exactly the drift a hand-written list produces.

How it is kept true

The table below is generated from settings.ScriptEnv() in internal/settings/scriptenv.go, and tests hold that table to the code:

  • Everything set is declared, by the right setter. The Go sources are walked as syntax – a map key, an assignment to an index, a KEY=VALUE string, os.Setenv – and the plugin’s put(e, "MCPX_...") calls are read, and every name found must be in the table with the part of mcpx that set it listed. A setter the table names that sets nothing fails too.
  • Everything read is declared. A lookup of an MCPX_ variable anywhere outside internal/settings must be either a setting read through the resolved settings, a variable in this table, or one of four lookups on an allowlist that carries the reason for each – three happen before any settings exist, the fourth where there is no App to ask. An entry nothing needs fails the test. A variable the registry owns may not be read by name even when it is also here, because that would ignore the config file and the flag.
  • The “read by” column is checked. If it says the generated client reads a variable, the generated TypeScript does; if the plugin reads one, the column says so.
  • This page is current. The test regenerates the table and fails when it differs.

This table is what mcpx adds, not all a child gets

Every one of these is added to the environment mcpx itself was started with. A child gets the parent’s environment entire, and then these on top — unless its shebang replaces part of it: env -i starts the child from an empty environment, and env -u NAME removes just that name (both then get these on top). Both the dispatched path and the JavaScript path apply what the shebang asked for:

  • internal/runner/runner.go:359 — cmd.Env = append(os.Environ(), …), every script the runner starts
  • internal/adapter/adapter.go:239 — cmd.Env = os.Environ()
  • internal/mcpclient/stdio.go:66 — env = append(env, os.Environ()...), under InheritEnv, for every stdio MCP server the pool starts

So a script also sees SSH_AUTH_SOCK, GITHUB_TOKEN, AWS_*, and whatever else was in the shell that started the daemon. PATH is inherited rather than constructed, which means a daemon started from a shell with an unusual PATH passes it on.

script.permissions does not change this. It sandboxes the filesystem and the network through the runtime’s own flags; there is no allowlist or denylist for the environment, and nothing here is filtered.

This is worth knowing before putting a secret in the environment of a shell you then run mcpx serve from. It is stated rather than fixed because the alternative — an allowlist — would break every server that expects its own API key to arrive this way, which is most of them.

Configuration is not here

A variable here is a fact about one run – which session, which daemon, which file – handed from a parent to a child. A knob is a setting, with a configuration key, a flag, a /v1/settings row and an MCP tool; those are in configuration.md. Two names are both: MCPX_LOG_LEVEL and MCPX_DAEMON_ENDPOINT are written for a child, and a child that is mcpx reads each as the setting it names.

Some variables are read by mcpx and are neither: which configuration file to load (MCPX_CONFIG, the setting paths.configFile) and where the state lives (MCPX_STATE_DIR, MCPX_CACHE_DIR) are needed before there is a configuration to resolve settings from. They are settings, marked as such, and read by name for that reason.

The variables

variableset bywhenwhat it isread by
MCPX_SESSIONmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec); an event consumeralwaysthe session key this run’s tool calls are leased under: –session, else MCPX_SESSION_ID, else one minted for the runthe generated client, which sends it with every call
MCPX_SESSION_IDmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec); an event consumer; the opencode plugin; the plugin loader, for a discovered pluginthe CLI passes through the value it was given, which may be empty; the plugin sets it at every plugin.env levelthe session the host assigned. Unlike MCPX_SESSION it survives from one command to the next, so it is what session-scoped servers key onmcpx run inside the script or shell, as its session when –session is not given
MCPX_PARENT_SESSION_IDmcpx exec / mcpx run; the opencode pluginthe CLI passes through what it was given; the plugin at plugin.env=full, when there is a parentthe session that spawned this one, so a parent agent and its subagents can share a servermcpx run inside the script or shell, and the generated client for parent-session scoping
MCPX_EPHEMERALmcpx exec / mcpx runalways1 when the session key was minted for this run alone, so its instances may be stopped as soon as it endsthe generated client
MCPX_RUNmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec)alwaysthis execution’s run id, which groups the artifacts it producesthe generated client, when storing an artifact
MCPX_INPUTmcpx exec / mcpx runmcpx exec and mcpx run, locally and with –remote; not when mcpx serve collects the outputreport: nothing in this run can answer a server’s question mid-call, so a call that asks one exits 75 with the question instead of waiting for it to expirethe generated client, sent as X-Mcpx-Input
MCPX_HOOK_NAMEthe declared-hook runner, for a hook it runsalwaysthe declared name of the hook being runthe hook itself
MCPX_HOOK_PHASEthe declared-hook runner, for a hook it runs; the exec service (/v1/exec, mcpx_exec)alwaysthe hook’s phase; always observe for config-declared hooks, the exec hook’s phase (pre, post, onError, onSuccess) for a per-call exec hookthe hook itself
MCPX_HOOK_POINTthe exec service (/v1/exec, mcpx_exec)alwaysthe point the hook is attached to; script for per-call exec hooks in this versionthe hook itself
MCPX_HOOK_ERRORthe exec service (/v1/exec, mcpx_exec)only for a hook running because the script failed; truncated to 1024 bytesthe error the hook is running for, in one line; the full hook context is JSON on stdinthe hook itself
MCPX_HOOK_EVENT_KINDthe declared-hook runner, for a hook it runsalwaysthe kind of the event the hook is running for; the full event is JSON on stdinthe hook itself
MCPX_HOOK_EVENT_SEQthe declared-hook runner, for a hook it runsalwaysthe event’s sequence number on the daemon’s busthe hook itself
MCPX_HOOK_EVENT_SERVERthe declared-hook runner, for a hook it runsempty when the event names no serverthe server the event is aboutthe hook itself
MCPX_HOOK_EVENT_SESSIONthe declared-hook runner, for a hook it runsempty when the event belongs to no sessionthe session the event belongs tothe hook itself
MCPX_HOOK_EVENT_URIthe declared-hook runner, for a hook it runsempty when the event names no resourcethe resource URI the event is aboutthe hook itself
MCPX_ENDPOINTmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec); an event consumeralwaysthe daemon’s HTTP endpointthe generated client; the plugin as a fallback for MCPX_DAEMON_ENDPOINT
MCPX_SOCKETmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec); the plugin loader, for a discovered pluginempty when the daemon is remote, since its socket is not reachable from herethe daemon’s unix socket, which is faster than the endpoint. Also where a daemon binds and a client dials, for anyone who sets it by handthe generated client, mcpx run inside the script or shell, the plugin
MCPX_TRANSPORT_BEARER_TOKENmcpx exec / mcpx runonly when transport.bearerToken is configuredthe daemon’s bearer token, handed to the script so the generated client can authenticate to a daemon that requires it (#1975); the token itself stays out of the generated codethe generated client, mcpx run inside the script or shell, the plugin; also the setting transport.bearerToken
MCPX_SETTINGSmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec)only when the caller sent call settings with the run (for example a requested pool.callTimeout)the call-settings header for this run, carried into the script so the generated client forwards the caller’s per-call requests on the calls it makes on the script’s behalfthe generated client, which sends it as the x-mcpx-settings header
MCPX_ARTIFACTS_LOCALmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec)always1 when the daemon shares this filesystem, so artifact({path}) may hand over a path instead of the bytesthe generated client
MCPX_CWDmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec)the exec service only when the caller sent its working directorythe directory the caller ran mcpx from, which a remote run cannot discoverthe generated client (paths.cwd, and cwd scoping)
MCPX_PIDmcpx exec / mcpx run; the exec service (/v1/exec, mcpx_exec)alwaysthe pid of the mcpx process that started the scriptthe generated client, for pid scoping
MCPX_ENTRYthe runner, for every scriptalwaysthe file the runtime was started on: mcpx’s generated launcher, or the script itself under –no-launcher. For exec the script is a generated filethe generated client (paths.entry)
MCPX_CLIENTthe runner, for every scriptalwaysthe generated client modulethe generated client (paths.client)
MCPX_CONFIG_PATHmcpx exec / mcpx runalwaysthe nearest configuration file in effect, empty when there is nonethe generated client (paths.config)
MCPX_SCRIPT_DIRSmcpx exec / mcpx runalwaysthe directories searched for named scripts, nearest first, colon-separatedthe generated client (paths.scriptDirs)
MCPX_API_DECLARATION_SHA256mcpx exec / mcpx runalwaysthe fingerprint of a declared API’s declaration, set on the child a declared API launches: PoolID hashes the environment, so editing the declaration replaces the pool instead of keeping a child started for the old onethe pool, as a pool identity input (api serve: PoolID hashes the server environment)
MCPX_RUNTIMEthe runner, for every scriptalwaysdeno, bun or node; on a dispatched run, the interpreter’s name (sh, python3)the generated client; on a dispatched run, the script itself
MCPX_DISPATCHthe runner, for every scriptonly on a dispatched (non-JavaScript) runhow the interpreter was chosen: shebang or interpreter (script.interpreter)the script itself, and anyone reading its environment
MCPX_ALLOW_READthe runner, for every scriptonly when the permissions allow reading files1: the client’s file helpers may read. Without it they refuse, naming the permission, rather than leaving it to the sandboxthe generated client
MCPX_LOG_LEVELmcpx exec / mcpx runalwaysthe lowest level the script’s log keepsthe generated client; mcpx run inside the script or shell reads it as logging.level; also the setting logging.level
MCPX_LOG_SOURCEmcpx exec / mcpx runalwayswhich levels record a call site: a level name for that level and above, 0 for nonethe generated client
MCPX_DAEMON_ENDPOINTthe opencode pluginplugin.env=standard or above, once the plugin has chosen a daemonthe daemon this editor session settled on, so a shell command that runs mcpx talks to the same one instead of looking for its ownmcpx run inside the script or shell, as daemon.endpoint; the plugin, as the daemon to use; also the setting daemon.endpoint
MCPX_TRACE_IDSthe opencode pluginplugin.env=standard or above, when there is anything to sayJSON pairs such as [[“session_id”,“abc”],[“worktree”,“/p”]], extended at full with the parent session and the whole ancestrymcpx run inside the script or shell, which puts every pair on every log record
MCPX_CALL_IDthe opencode pluginplugin.env=standard or abovethe editor’s id for this one tool callthe command
MCPX_OPENCODE_CWDthe opencode pluginplugin.env=standard or abovethe directory the command runs in, which is not always the project rootthe command
MCPX_OPENCODE_DIRECTORYthe opencode pluginplugin.env=standard or abovethe project directory the editor openedthe command
MCPX_OPENCODE_WORKTREEthe opencode pluginplugin.env=standard or abovethe project’s worktreethe command
MCPX_WORKTREE_NAMEthe opencode pluginplugin.env=standard or above, when there is a worktreethe worktree’s last path elementthe command
MCPX_PROJECT_IDthe opencode pluginplugin.env=standard or abovethe editor’s project idthe command
MCPX_HARNESSthe opencode pluginplugin.env=standard or aboveopencodethe command
MCPX_HARNESS_VERSIONthe opencode pluginplugin.env=standard or abovethe editor’s versionthe command
MCPX_HARNESS_PIDthe opencode pluginplugin.env=standard or abovethe editor’s pidthe command
MCPX_HARNESS_STARTEDthe opencode pluginplugin.env=standard or abovewhen the plugin startedthe command
MCPX_SHELL_SEQthe opencode pluginplugin.env=standard or abovea counter of shell commands in this editor processthe command
MCPX_SESSION_TITLEthe opencode pluginplugin.env=fullthe session’s titlethe command
MCPX_SESSION_DIRECTORYthe opencode pluginplugin.env=fullthe session’s directorythe command
MCPX_SESSION_VERSIONthe opencode pluginplugin.env=fullthe editor version that created the sessionthe command
MCPX_SESSION_DEPTHthe opencode pluginplugin.env=fullhow many parents the session hasthe command
MCPX_SESSION_CREATEDthe opencode pluginplugin.env=full, when knownwhen the session was created, ISO 8601the command
MCPX_SESSION_AGE_MSthe opencode pluginplugin.env=full, when knownthe session’s age when the command started, in millisecondsthe command

Examples

Four scripts you can run as they are. Each one calls the same deterministic fixture server, prints something short, and has its output checked into examples/ beside it. go test ./examples/ runs every script and compares stdout with that file, so an example that stops being true fails the build instead of sitting here looking plausible.

They are deliberately small. tools is on globalThis, so none of them imports anything, and the first one is barely longer than the command that runs it.

Run them from pkgs/mcpx. The first run writes mcpx-client.ts and its declarations beside the script, which is how mcpx run works for any file; the .gitignore in that directory keeps them out of git.

Set up the demo server first

Every script calls the demo namespace, which is not a public server: it is the deterministic fixture in internal/testsupport/fakemcp, and outside the test harness nothing configures it for you. From a clean checkout, build the fixture and mcpx itself, then give mcpx a config that maps demo to the fixture — the same arrangement examples/examples_test.go builds for itself:

cd pkgs/mcpx
go build -o /tmp/mcpx-bin/mcpx ./cmd/mcpx
go build -o /tmp/mcpx-bin/fakemcp ./internal/testsupport/fakemcp
cat > /tmp/mcpx-bin/demo.json <<'JSON'
{
  "mcpServers": {
    "demo": { "command": "/tmp/mcpx-bin/fakemcp" }
  }
}
JSON
export MCPX_CONFIG=/tmp/mcpx-bin/demo.json
export PATH="/tmp/mcpx-bin:$PATH"

MCPX_CONFIG is how the mcpx run commands below pick that config up. With it set, the first example prints exactly what the page shows; without it, the same command fails, because no server answers to demo.

Call one tool

The whole shape: top-level await, one argument object, print the result. demo.echo answers with a text block, which arrives as a string.

// Call one tool and print what it returns.
//
// Nothing is imported: `tools` is already on globalThis.

const echoed = await tools.demo.echo({ message: "hello from a script" });

console.log("demo.echo:", String(echoed));
mcpx run examples/call-a-tool.ts
demo.echo: hello from a script

Find a tool from inside the script

search() and describe() read the generated client, so looking something up costs no round trip to the server. This one lists the namespace, reads one tool’s signature, then calls two tools — one returning text, one returning structuredContent, which arrives already parsed.

// Find out what a namespace offers from inside the script, then call two of
// the tools it lists.
//
// `search()` and `describe()` read the generated client, so they cost no
// round trip to the server.

console.log("tools:", Object.keys(tools.demo).join(", "));
console.log(describe("demo.echo").trim());

const echoed = await tools.demo.echo({ message: "two tools" });
const structured = await tools.demo.structured();

console.log("echo:", String(echoed));
console.log("structured:", JSON.stringify(structured));
mcpx run examples/discover.ts
tools: boom, chatty, echo, fancy_name, open, slow, state, structured, wipe
demo.echo({ message })
      Echo a message back.
echo: two tools
structured: {"n":42}

State that survives between calls

open records a value in the server process and state reads it back. The daemon keeps one process for the session, so a later call sees what an earlier one did — no local variable involved. The script prints only whether its own two values are there, because the full list also holds whatever an earlier run recorded.

log() is on globalThis too, and writes to the durable log on stderr. The expected output below is stdout, which is what the test compares.

// `open` records a value in the server process and `state` reads it back, so
// a later call in the same script can see what an earlier one did.
//
// log() writes to the durable log on stderr; console.log is the script's
// stdout, which is what the expected output below holds.

log("recording two values");

console.log(String(await tools.demo.open({ value: "alpha" })));
console.log(String(await tools.demo.open({ value: "beta" })));

const state = await tools.demo.state();

console.log("alpha recorded:", state.seen.includes("alpha"));
console.log("beta recorded:", state.seen.includes("beta"));
mcpx run examples/stateful.ts
opened alpha
opened beta
alpha recorded: true
beta recorded: true

A recipe with parameters

The header declares the holes and the body uses @name, which is the whole recipe convention. mcpx run refuses a file like this — it cannot fill the holes — so it is saved and run with values instead. times has a default and is not passed here.

// Echo a message a few times through the demo server.
//
// The header declares the holes; `mcpx recipes run` fills them. A plain
// `mcpx run` refuses this file, because @message would reach the runtime
// unfilled.
//
// @param message:string   what to echo
// @param times:number = 2   how many times

for (let i = 0; i < @times; i++) {
  console.log(String(await tools.demo.echo({ message: @message })));
}
mcpx recipes save recipe-echo examples/recipe-echo.ts
mcpx recipes run recipe-echo message="piped through a recipe"

The mcpx: recipe recipe-echo line goes to stderr; the two echoed lines are stdout:

piped through a recipe
piped through a recipe

Every one of these is executed by go test ./... on each change, against the same fixture server, so the output above is what the scripts print today rather than what they printed when the page was written. For why the fixture is a fake and not a real server, see the FAQ.

exec and artifacts

A script produced a screenshot. Who gets it, and what does it cost them?


The thing that was wrong

mcpx exec ran the script in the CLI process. The CLI generated the client, spawned deno, and the script called back into the daemon over its socket. That is the right arrangement for a person at a terminal — the script owns stdout, reads real standard input, and a relative path in it means what it means on the command line.

It is the only arrangement, and that was the problem. Two callers could not use it at all:

  • The opencode plugin holds a unix socket and nothing else. It has no mcpx binary to spawn. It could list tools, call one, read the log — and could not run a script, which is the thing mcpx exists for.
  • A daemon on another machine could not be asked to run one. The servers are over there; the script ran over here and reached across the network for every single call.

So POST /v1/exec: the daemon runs the script. The generated client it writes points at the daemon’s own socket, so the calls that used to cross a network now cross a socket, and a caller with no binary has one request to make.

The refactor that made that safe is internal/execsvc. Two implementations of “run a script” would be two sets of bugs and a script that behaves differently depending on who asked — which is exactly what this whole feature is trying to stop. There is one.

Why the CLI still builds its own runner options

execsvc.Run is the wire entry point: everything arrives as ExecOptions and nothing is inherited from a terminal that is not there.

execsvc.RunWith takes runner options the caller assembled. The CLI uses it, because mcpx run genuinely has more knobs than /v1/exec does — launcher templates, five injection phases, --keep, a typecheck mode, placeholder files — and flattening all of that into the wire type would make the wire type a mirror of one command’s flag list.

What must not be duplicated is everything after the process starts: which frames are produced, how artifacts are reconciled, when media is intercepted, how delivery is applied. That is all in RunWith, and it is the same code for both. Hooks the caller already set are chained rather than replaced, so the CLI keeps rendering logs to a terminal while the service collects the same records for the result.

Where a script runs

exec.where is auto, local or remote, spelled --local and --remote at the command line.

Auto runs locally when the daemon is on this machine and on the daemon when it is not. That is the rule that makes the common case right without anyone choosing: a person gets a terminal-shaped run, and a configured remote endpoint gets a remote one, because running locally against a remote daemon means every call crosses the network and every file the script writes lands on the wrong side.

--local against a remote endpoint is refused rather than attempted. It does work, in the sense that the script runs and the calls succeed; it just quietly produces a different artifact story, and saying so beats a subtly different result.

A remote run sends the script’s text, not its path. A path would mean the daemon’s filesystem, and somebody typing mcpx run ./report.ts --remote means the file in front of them. The consequence is that relative imports do not survive the trip: the daemon writes the text to its own disk, and a sibling module is not beside it. --export does survive, because the launcher imports the written file and calls the named export there.

Every flag that shapes the program travels with it: --env, --typecheck, --launcher/--no-launcher, --allow-repeat, --no-capture-console, --export, and the five phases (--before, --prefix, --suffix, --on-success, --on-error). The CLI resolves them – reading any file a phase or launcher names on its side – and sends source, so the daemon runs the program that would have run locally. Until #192 the remote path sent five of these and silently dropped the rest; TestEveryExecFlagChangesSomething now runs every row both ways.

ExecOptions

One flat object that grows, rather than a new endpoint per capability. This is step 7 of the issue and it is the right shape: the caller declares what it can receive, the script declares what it produced, mcpx handles delivery.

type ExecOptions = {
  runtime?: "auto" | "deno" | "bun" | "node";
  timeout?: string;
  permissions?: string;
  placeholders?: Record<string, unknown>;
  session?: string;
  cwd?: string;
  env?: Record<string, string>;
  stdin?: string;
  output?: "text" | "structured" | "stream";
  capabilities?: string[];
  artifacts?: {
    delivery?: "reference" | "inline" | "stream";
    maxBytes?: number;
    sharedFs?: string;
    dir?: string;
  };
  task?: { ttl: number };
  ns?: string[];
  args?: string[];
  export?: string;
  typecheck?: "off" | "on" | "strict";
  launcher?: string;
  launcherName?: string; // template text, or "none"
  allowRepeat?: string[];
  captureConsole?: boolean;
  phases?: { before?: string[]; prefix?: string[]; onSuccess?: string[]; onError?: string[]; suffix?: string[] };
};

cwd, env and stdin are the caller’s context, not the daemon’s. A remote daemon has a different working directory and a different environment, and a script that reads either should see the one belonging to whoever asked. A local caller passes its own, which is how the two cases stay identical.

Three fields are not in the issue’s list. ns and args are there because mcpx exec --ns and mcpx run script a b already exist, and an option the CLI has and /v1 does not is a parity hole — the one thing this repository checks for automatically. dir sits beside sharedFs because they are different statements: sharedFs is “I can see your disk”, dir is “put them here for me”. They happen to be implemented the same way when the caller is local, and they are not the same claim.

task reuses internal/tasks unchanged. A long exec with artifacts is a task whose result holds references, which already fits tasks/result.

Output shapes

Three renderings of one run.

  • text — the CLI default. stdout is the script’s answer and a person reading it wants it unadorned. It goes to the terminal as it is produced, not collected and printed at the end; a long run that printed as it went must keep doing so.
  • structured — the /v1 and MCP default. One document: {result, emits[], logs[], stdout, artifacts[], exitCode, durationMs, error?}. A caller over HTTP wants fields, not text it has to scrape.
  • stream — frames as they happen. SSE by default because it is plain HTTP and a browser consumes it with no code; NDJSON on Accept: application/x-ndjson, because a program with a JSON decoder should not have to strip data: off every line first.

result is the script’s stdout parsed when stdout is JSON. That rule is inherited from mcpx --json run rather than invented, so the two agree.

The frame order, and why it is the order

start{runId}
hook{phase:"before",...}       (one per before hook, in declaration order)
log | emit | stdout | call{server,tool,argsHash,durationMs,ok,error?,destructiveHint?}   (interleaved, as they happen)
artifact{id,name,mime,size,sha256,uri}
hook{phase:"onSuccess"|"onError",...}
result | error
hook{phase:"suffix",...}       (one per suffix hook, either path)
end{exitCode,durationMs}
---- only for delivery:"stream" ----
artifact.chunk{id,seq,data}
artifact.end{id}

A call frame records one tool call the script made while it ran: the server and tool, a hash of the arguments (never the arguments – a stream is a thing other people read), how long the call took, whether it succeeded, and the tool’s destructiveHint when its annotations declare one. Call frames appear on runs the daemon itself executes, whose call path can reach the run’s stream; a run whose calls arrive at a daemon that is not executing it has no stream there to join.

Hook frames sit where the hook ran, so the answer a consumer reads is the final one: an onSuccess/onError hook frame precedes the result/error frame it may have replaced (its frame carries the original when it changed the value), and a suffix hook frame follows the outcome, before end. A replaced error produces no error frame at all.

Artifact metadata comes before end; artifact bodies come strictly after it. That is the whole point of the design, and it is step 5 of the issue: a streaming consumer reads the emits and the return value, decides it does not need the 4 MB video, and disconnects at a frame boundary having paid for none of it.

A disconnect is not merely tolerated. The sink returning an error cancels the run context, which kills the process group; and every file the CLI writes from a stream goes to a temporary name and is renamed on artifact.end. A cancelled stream must not leave something that looks like a complete file, because the next thing to read that directory cannot tell the difference.

Artifacts

The script API is the same everywhere

const shot = await chrome_devtools.take_screenshot({ format: "png" });
await artifact("checkout.png", shot);
return { total };

data may be a string, a Uint8Array, an ArrayBuffer, { path }, an MCP image/audio/resource content item, or a tool result carrying one on .raw. Accepting the content item directly is what makes the common case one line: a screenshot arrives as {type:"image",data,mimeType} and that is exactly what gets passed in.

The script never asks where it is running. The issue’s step 1 — if (mcpx.session.isRemote) — was rejected for that reason: an author who has to know about remoteness writes two code paths and one of them is never tested.

What actually happens underneath differs, and only in a way nobody has to think about. artifact() POSTs to /v1/artifacts over the transport the generated client already has. When the runner set MCPX_ARTIFACTS_LOCAL=1 — which it does when a unix socket proves the daemon shares this filesystem — a {path} is sent as a header and the daemon reads, hashes, and copies the file into the store. Otherwise the bytes travel once to the daemon and are then fetched by whoever wants them.

Remoteness is declared, never inferred. A unix socket can be ssh-forwarded and loopback TCP can be a container with its own filesystem; neither transport tells you whether the caller can see the daemon’s disk. So the caller says so, with capabilities: ["artifacts"] and optionally sharedFs.

An ssh:// endpoint is the clearest case of sharedFs: false: the bytes cross a process boundary on another machine, so the caller uploads a {path} artifact’s bytes rather than giving the daemon a path it can read.

The store

Content-addressed by sha256 under <state>/artifacts/blobs/ab/cdef..., with a SQLite index beside it. SQLite because it is already a dependency and because “list the artifacts of this run” is a query rather than a directory walk.

The handle is not the hash. A content hash is guessable by anyone who can guess the content — and for a screenshot of a public page, that is everyone. So an artifact is served by a separate random 128-bit id mapped to the hash. That id is the entire access control until OAuth privilege levels land, which is why it is sized as a secret rather than as an identifier. When scopes arrive, the natural shape is that the session that produced an artifact owns it and a privileged scope can read any of them; the index already carries the session for exactly that.

Two registrations of the same bytes share one blob and get two handles. The quota counts distinct content, so a script that produces the same screenshot twice pays once. Deleting a registration only removes the body when no other registration refers to it.

Everything is written to a temporary name and renamed on completion — in the store, in --artifacts-dir, and in the stream consumer. A cancelled upload leaves nothing a later reader could mistake for a whole file.

Names are untrusted

The name comes from the script, and a script is not trusted with a path. SanitizeName takes the last component (so ../../etc/passwd is passwd), keeps [A-Za-z0-9.-_+], turns spaces into hyphens and everything else into underscores, strips leading and trailing dots, and truncates to artifacts.nameMaxLength while keeping the extension. An empty result becomes artifact, because a file has to be called something.

Removal rather than rejection: a caller whose artifact is refused over its name has lost the file, and the name is decoration — the artifact is reached by id.

Collisions get a numeric suffix before the extension — shot-1.png, not shot.png-1 — so the file still opens in whatever the extension implies. Nothing is ever overwritten: two files called shot.png are two files, and silently keeping one of them is data loss. The reservation uses O_CREAT|O_EXCL, because testing with Stat and then creating is a race two concurrent runs of the same script would lose.

Delivery

modewhat crosseswhen
reference (default)an id and a URIalways correct; the only mode whose cost does not scale with what the script happened to produce
inlinebase64 in the structured result, bounded by artifacts.inlineMaxBytes and the caller’s maxBytesa caller that cannot make a second request
streamframes after endone response, cancellable

A sharedFs or dir lets the caller read the destination directly, but the store still copies the bytes into that directory; this saves a later transfer, not filesystem I/O.

Reference is the default because of the round-trip arithmetic. Over a unix socket a second request costs about 0.2 ms, so separate GETs are free locally and bring Range and parallel downloads with them. Remotely a single stream is better, which is why stream exists — and artifacts remain fetchable by id afterwards either way, so choosing the stream costs nothing.

inline’s ceiling is deliberately well under the per-artifact ceiling. Inline delivery puts bytes in the caller’s context, which is the cost this whole feature exists to avoid.

Intercepting upstream media

When a structured or streamed result — or an emit — contains an MCP image, audio or blob resource block, and the caller declared artifacts, the block is stored and replaced by a resource_link carrying the URI, the name, the type and the size.

This is the local win, and it is worth having with no remote daemon anywhere. chrome-devtools returns a screenshot as base64 image content; left alone that lands in an agent’s context, where one screenshot costs more than the rest of the task and where nobody will ever look at it.

It happens at the output boundary, not inside the script. A script that received a handle instead of the bytes could not inspect the image it just took — measure it, crop it, compare two — which is a real thing scripts do. So the script sees whatever the upstream server sent, and only what crosses back to the caller is rewritten. artifacts.interceptImages turns it off.

Reaching an artifact

surfacehow
MCPresource_link in the mcpx_exec result, then resources/read on mcpx://artifacts/{id}; also resources/list and a resource template
/v1GET /v1/artifacts/{id} with Range, GET /v1/artifacts?run=…, DELETE /v1/artifacts/{id}
CLImcpx exec --artifacts-dir ./out

GET /v1/artifacts/{id} goes through http.ServeContent, which brings Range, conditional requests and 206 for free — and Range is what makes reading the tail of a large artifact cheap, one of the three things printing the file could not do. Content-Disposition is attachment with the sanitised name, because a browser deciding to render somebody else’s bytes inline is how a store of arbitrary files becomes an XSS surface.

resources/read carries text, so a binary body comes back base64 with its real type stated. That is lossy in exactly one way — the client decodes — and it is what the protocol offers. A client that wants raw bytes has the /v1 route.

One edit outside this area

mcpserver.dispatch returns a string, which cannot carry a resource_link. Threading a content array through a dozen cases to serve one of them would make eleven signatures worse to serve the twelfth. So mcpx_exec encodes its blocks into the string it already returns (mcpserver.EncodeResult), and tools/call decodes it in exactly one place. EncodeResult returns the text unchanged when there are no blocks, so every other tool is byte-for-byte what it was.

Settings

Everything is declared in internal/settings/exec.go. It is not declared once: unlike internal/settings/consumer.go and internal/settings/wire.go, which read defaults.*, every Default: in exec.go is an inline string literal ("120s", "64MiB", "10m", …), so each value that also lives in defaults.json is written in two places with nothing holding them equal. Three of them — exec.where, artifacts.enabled and artifacts.dir — have no defaults.json key at all. The values below are what both say today.

settingdefaultwhat it decides
exec.timeout120show long a script may run, wherever it runs
exec.outputtexttext, structured or stream
exec.whereautolocal, remote, or decided by whether the daemon is
artifacts.enabledtruewhether the store exists at all
artifacts.dir—--artifacts-dir
artifacts.deliveryreferencereference, inline or stream
artifacts.ttl24hretention before collection
artifacts.maxBytes64MiBthe largest single artifact
artifacts.quota1GiBthe total, over distinct content
artifacts.inlineMaxBytes1MiBthe largest that may be base64’d into a result
artifacts.chunkBytes256KiBone streamed body frame
artifacts.gcInterval10mhow often expired artifacts are swept
artifacts.interceptImagestruerewrite inline media at the boundary

Exceeding maxBytes is an error, not a truncation. A truncated screenshot is worse than a refused one, because it looks like it worked.

nameMaxLength, nameCollisionLimit, idBytes and listLimit live in defaults.json without a registry entry: they are read at package initialisation and a configuration file arriving later could not change them, and a setting that does nothing is worse than no setting. (This paragraph used to offer plumbing.eventHistory as the comparison. That one is a setting — events.history, internal/settings/wire.go:239, flag --events-history — so it was the wrong example.)

Not done

  • The other direction. An inbox — uploading a file a script needs as input, with a script-side input("name") — is out of scope for this cut. Nothing here prevents it: POST /v1/artifacts already stores by content and the index already carries a session.
  • OAuth scoping. Artifacts are served by an unguessable id on an unauthenticated API, which is the same access control every other /v1 route has. The index carries the session so a scope check has something to check.
  • multipart/mixed. The issue offers it as an alternative to base64 in the stream. NDJSON and SSE carry chunks base64’d, which costs 33% on the wire. Worth revisiting; not worth a second framing before the first one has a user.
  • Relative imports on a remote run. They need sibling modules on the runtime’s disk, and a remote run sends one file’s text.

FAQ

The five questions a skeptical first-time reader asks first, in the order they ask them, answered in the same voice.


Short answers here; the long ones live in the pages each answer links to. Every question keeps the wording it was written in.

“so why’d you get rid of all the mcp and then make one, do you like mcp or not”

We like MCP fine. What we got rid of is tool schemas sitting in the model’s context, not MCP itself: mcpx is an MCP client of every server you configure, across four protocol revisions, reading the same mcpServers config every other host reads, and “mcpx puts every MCP server behind one command so their schemas stay out of the model’s context until something asks for them”. The server we made is deliberately tiny. “Exposing three hundred tools over MCP would rebuild the problem mcpx was built to solve, with extra steps”, so it is /mcp on the daemon, mcpx serve --transport stdio, and mcpx_exec as the one tool that matters.

More: the story.

“i mean, opencodemode is right there”

It is, and not only in v2. opencode v1 ships code mode behind OPENCODE_EXPERIMENTAL_CODE_MODE, v2 has it on by default, and both accept TypeScript. Its version is better confined: a purpose-built interpreter that runs only the language features upstream implemented, plus OAuth and elicitation for remote servers. What it does not do is what mcpx is for. It only runs inside a model turn — there is no opencode2 exec — one daemon here serves every harness with a real process per session for stateful servers, and our scripts run on a real runtime with imports and a filesystem. Our own recommendation still stands: let code mode own ordinary MCP inside opencode’s loop, and use mcpx for stateful servers and everything outside a turn.

More: the opencode plugin.

“is it safe?”, no.

No. Scripts run on a real runtime with your authority — filesystem, network, subprocesses. script.permissions defaults to all. What a narrower profile does depends on the runtime, and the defaults say so exactly (internal/defaults/defaults.json): on Deno the profiles become Deno permission flags, and on node readnet / read-net are enforced with --permission --allow-fs-read=*. net, read, and strict are defined for Deno only. Ask for a named profile that has no entry for the runtime you chose and mcpx refuses to run the script at all — “permission profile %q defines no flags for %s, so %s cannot enforce it” (internal/runner/runtimes.go) — it never runs the script unenforced and pretends otherwise. Bun has no permission model, so no narrower profile is defined for it and every narrower ask there is refused the same way. The profiles that are defined for bun and node — all, none, off, unsandboxed — carry an empty flag list by design: defined, and deliberately unrestricted, not enforcement. The daemon API is unauthenticated. It listens on loopback and on a socket only you can open, and binding it wider prints a warning. mcpx prompt hands you a generated script instead of running it, because generated code runs with your credentials. So treat a script like a shell command you are about to paste: configure only servers you would run by hand, and do not expose the port. That is the whole answer.

More: the script contract.

“but why would you..”

Three reasons, then the history. Context cost: tool schemas sit in every request whether or not the tools get used, so mcpx’s always-on cost is zero — “there are 27 Chrome tools live on this machine and not one byte of their schemas is in the agent’s context”. Concurrency: two agents sharing one browser corrupt each other, so each session gets its own process. Reach: the same tools from a shell, CI or any harness. The history is that this replaced lootbox, a fork 51 commits ahead of a quiet upstream, where discovery was uncached (about 650 ms per request) and the architecture “cannot express the one thing you actually need (more than one Chrome)”.

More: the assessment.

“but can i get some examples?”

Yes. examples/ holds short scripts that CI runs on every change, against a deterministic fixture server, paired with the output they print today — so an example cannot quietly rot, and one of them is a real recipe (mcpx recipes). The gallery page pairs each script with its command and its expected output; copy one, change the server name, run it. They are deliberately small, and none of them needs Chrome or the network. If one is wrong, that is a bug worth reporting, because the only reason these are trustworthy is that they fail the build when they stop being true.

More: the examples gallery.

Finding the repository without git

How the repo and worktree scopes decide which repository a caller is in, why that no longer runs git, and what was decided for every layout git understands.

created:      2026-09-30T07:30:00-05:00
last-updated: 2026-09-30T07:30:00-05:00
status:       implemented

This is the decision record for the change. docs/container.md §3 is the short version; it lives here rather than under docs/decisions/ because that directory belongs to another area.


1. Two questions

A server scoped repo gets one process per clone – every linked worktree of one repository shares it. A server scoped worktree gets one per checkout. The keys are the answers to two questions git already has commands for:

scopekeygit’s command
repothe common git directorygit rev-parse --git-common-dir
worktreethe top of the checkoutgit rev-parse --show-toplevel

Until this change mcpx asked exactly those, by forking git twice per directory and caching both answers – negative ones included – for the daemon’s lifetime. That had four costs, three of which were only found by testing the replacement:

  1. 105 MB of the container image. Debian’s git depends on perl outright.
  2. It did not work in the container anyway. A checkout mounted into the image is owned by the host’s uid, the image runs as uid 1000, and git refuses a repository someone else owns (safe.directory). Every mounted checkout, measured: fatal: detected dubious ownership in repository at '/work/proj'. The old image shipped git so that these scopes would work, and in the ordinary way of using it they fell back to per-directory keys.
  3. The daemon’s environment leaked into every caller’s key. An autostarted daemon inherits the environment of whoever ran mcpx first. A git hook exports GIT_DIR; a hook that runs mcpx leaves a daemon carrying it for hours. With GIT_DIR absolute, every later caller – from any directory – was keyed to that one repository. With GIT_DIR=.git, as hooks set it, every caller outside a repository root degraded. Both are pinned by TestTheDaemonsOwnGitDirDoesNotPinEveryCaller, which fails against the old code.
  4. The cache made the first answer permanent. A directory that became a repository after the first call there stayed “not a repository” until the daemon exited.

2. How it works now

Finding a repository is a walk up the directory tree reading a handful of small files. Git’s rules for that walk are fixed, and they are in its source: setup.c (setup_git_directory_gently_1, read_gitfile_gently, is_git_directory, check_repository_format_gently, the three setup_*_git_dir functions) and the config parser in config.c. internal/config/gitdiscover.go and gitformat.go are a port of those, from git v2.55.0, function by function and named after the originals so the two can be read side by side.

At each directory, starting from the caller’s cwd with every symlink resolved (git starts from getcwd(), which has already resolved them):

  1. .git – a directory that is a repository, or a file naming one;
  2. the directory itself, as a bare repository;
  3. the parent, unless a ceiling or a filesystem boundary says stop.

Once found, the common directory comes from the commondir file if there is one, and the worktree from where .git was found – unless the repository’s config says otherwise through core.bare or core.worktree.

Native discovery reads HEAD, commondir, a .git file, and from config three keys – core.repositoryformatversion, core.bare, core.worktree – plus whatever extensions.* holds, since an unknown extension is what makes a repository unreadable. When extensions.worktreeConfig is on it reads the worktree’s config.worktree as well. It tests objects/ and refs/ for existence without reading them, which is the rest of what is_git_directory decides on. Nothing else, and it executes nothing.

Every outcome is one of four:

outcomemeaningwhat the scope does
foundthe same answer git giveskeys by it
not a repositorygit would say so tookeys by directory
brokensomething on disk git dies on – a .git file naming nothingkeys by directory; doctor warns, with the fix
unsurea layout this port will not vouch forasks git

Git is the fallback only for “unsure”. It is asked once, as git rev-parse --path-format=absolute --git-common-dir --show-toplevel: the first answer is printed before the second fails, so a bare repository still yields its common directory from one process. It runs with git’s own list of repository-local variables removed (GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY and eleven more – git rev-parse --local-env-vars), so it answers the question native discovery answers. With no git on PATH, “unsure” keys by directory, and the reason says what could not be read and that git is not there to ask.

Nothing is cached. One resolution five directories deep costs 0.073–0.081 ms (BenchmarkDiscoverGit, -benchtime 300x -count=5, M3 Pro / macOS / APFS); forking git rev-parse --show-toplevel from the same place costs 9.7 ms on the same machine, so the native answer is about 125× cheaper. A cache would save under a tenth of a millisecond per call and bring back the stale answers of §1.4.

mcpx doctor reports what the two scopes resolve to from the current directory and what resolved them. A missing git is not a warning; it becomes one only when a repository here needs it.

3. Every case, and what was decided

“Parity” means the case is built with the real git and native’s answer is compared with git rev-parse for both questions. Test names are in internal/config/gitdiscover_test.go unless marked e2e.

Checkouts

casedecisiontest
main clone, from its root or any depth belowsame as gitparity: main clone
linked worktree, git’s relative commondir (../..)same as git: .git file → private dir → commondir → common dirparity: linked worktree with git's relative commondir
linked worktree, absolute commondirsameparity
linked worktree with relative paths (worktree.useRelativePaths, git 2.48; sets extensions.relativeWorktrees and format 1)same; the extension is one native knowsparity
linked worktree moved by handsame as git: still resolves, from its new place; the stale back-pointer is not consultedparity
main clone moved: its worktrees’ .git files name nothingsame as git: an error, not a fallback. doctor warns and names git worktree repairparity; TestWithoutGitABrokenGitfileIsAWarning
submoduleits own repository, as git says: common dir .git/modules/<name>, top the submodule’s directory. A submodule has its own HEAD, objects and history; a server scoped to “the repository” that saw the superproject’s instead would be looking at the wrong codeparity: submodule is its own repository (built from a local path, no network)
nested repositoriesinnermost wins, as in gitparity
--separate-git-dir, a relative .git file, CRLF endingssameparity
.git file path through a symlink and ..resolved by the kernel, not lexically, as git’s chdir doesparity
.git is a symlink to a git directory elsewheresame: the symlink’s targetparity
symlinked checkout; /tmp vs /private/tmpthe physical path, as git; both spellings key alikeparity; TestGitParityTmpAndPrivateTmp
a directory that is both a bare repository and holds a .git.git wins, as in git’s orderparity

Git directories

casedecisiontest
bare repository, from itself or inside itcommon dir yes, worktree no – worktree keys by directoryparity
inside .git, or a worktree’s private dirsame: a git directory, no worktreeparity
worktree of a bare clonehas a worktree: a linked worktree ignores the common core.bareparity
bare clone with extensions.worktreeConfigits worktrees become bare, as in git – git’s documented trap, reproduced rather than correctedparity
HEAD as a symlink into refs/, detached SHA-1, detached SHA-256validparity
reftable repositorysameparity
stray .git directory that is not a repositoryskipped; discovery continues upwardparity
nested repository whose HEAD is almost valid (40 non-hex characters, a symlink out of refs/, ref: outside refs/), or has no objects/skipped, as git validates itparity

.git files and entries git refuses

Git stops at these rather than skipping them, so mcpx does too – skipping would find the enclosing repository and share its instance. Every one is built inside a real repository so that skipping would be caught.

casetest
garbage; gitdir: without the space; no path; a path that does not exist; a path that is not a repository; trailing spaces (part of the path, as in git)parity: broken .git file: …
a .git file over git’s 1 MiB ceiling (git.gitfileMaxBytes)parity
a .git that is neither file nor directory (fifo)parity against git ≥ 2.54, which made this fatal; older git skipped it, and mcpx follows current git
an empty commondirparity
a directory discovery cannot look insidea directory discovery cannot look inside (git cannot chdir there at all)
cwd that does not exist, or is a filecwd that does not exist, and cwd that is a file

The repository’s config

casedecisiontest
core.worktree absolute, relative (to the git dir, not the common dir), or with only its last component missingsame as gitparity
core.worktree git cannot entergit dies before printing either answer, so both scopes fall backparity
core.bare; core.bare with core.worktreeno worktree (git warns that the pair makes no sense)parity
per-worktree config.worktree with worktreeConfigread, as git reads itparity
no config; a config without repositoryformatversionas git: everything else in it is discarded, so core.bare = true there means nothingparity
[include] setting core.worktreeignored, as git ignores it here: the repository format is read without includesparity
upper-case names, [core "x"], [foo "core"], [core.x], quoting, comments, continuation lines, a BOM, CRLF, bare keys, numeric booleansparsed as git parses themparity; TestGitConfigParserAgainstGitsOwn compares values with git config --file
an unknown extension in a version-0 repositoryignored, as git doesparity
format version > 1; an unknown extension in version 1; a version-1 extension in version 0; a syntax error; a value git refuses (bare = maybe, objectformat = md5, a bare worktree key); a value git takes that this port does not parse (bare = 1k)unsure → git. The git on the machine may be newer than this portparity, marked unsure

Environment

variabledecisiontest
GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY and the rest of git’s local listNot honoured, and removed from the fallback’s environment. The key is computed in the daemon, for many callers, and the daemon’s environment belongs to whoever started it – honouring it is §1.3. The caller’s variables never reach the daemon (CallContext carries the cwd, not the environment), and they do not need to: everywhere git itself sets GIT_DIR – hooks, aliases, submodule foreach – it also puts the cwd inside that repository, so discovery from the cwd reaches the same answerTestGitEnvironmentThatNamesARepositoryIsIgnored; TestGitLocalEnvCoversWhatGitCallsLocal checks the list against the installed git; e2e TestTheDaemonsOwnGitDirDoesNotPinEveryCaller
GIT_CEILING_DIRECTORIESHonoured, with git’s exact rules: relative entries ignored, entries symlink-resolved unless they follow an empty entry, a ceiling never examined from below it but honoured-as-absent when the walk starts there. It can only make keys narrower, and it lives in shell profiles, so a user who set it expects mcpx to agree with their shellparity: TestGitParityCeilingDirectories, eight cases
GIT_DISCOVERY_ACROSS_FILESYSTEM, and mount pointsHonoured: discovery stops where the device changes, as git’s does. A value that is not a boolean is unsureTestDiscoveryStopsAtAFilesystemBoundary (a boundary cannot be mounted without privileges, so the device lookup is substituted); verified with a real tmpfs in the image, docs/container.md §3

Deliberate differences from git

casegitmcpxwhytest
a repository owned by another user (safe.directory)refusesresolvessafe.directory stops git running configuration from a repository someone else controls. Native discovery runs nothing from it; it reads three small files and five keys. And the refusal is exactly what broke the container (§1.2). The fallback does not override git’s check, so an unsure layout in a foreign repository still falls back, with git’s reasonTestGitOwnershipIsNotCheckedNatively
an implicit bare repository under safe.bareRepository=explicitrefusesresolvesthe same reasoning: the setting protects against embedded bare repositories’ configurationTestGitSafeBareRepositoryIsNotCheckedNatively
a cwd spelled in the wrong case on a case-insensitive volumeon-disk spellingthe spelling givenGetting the on-disk spelling needs F_GETPATH, reachable from Go only outside the unsafe rules; the cwd scope has always behaved this way; the error over-isolates. Belongs in canonical() for every scope: #190TestGitCaseOfAWrongCaseCwdIsKept asserts the difference is case and nothing else
git older than 2.31 as the fallbackprints --path-format=absolute backrefuses its answerthe echoed option would otherwise be read as a path. Only reachable for unsure layoutsTestGitFallbackRejectsAGitWithoutPathFormat

Windows is not handled: drive letters, \, and //server/share ceilings are not ported, because mcpx does not build there (internal/runner uses Setpgid and Kill).

4. How it was tested

Parity. 54 subtests in six TestGitParity* functions build repositories with the real git in a fenced temporary root – GIT_CEILING_DIRECTORIES at its parent, a private global config, no system config, none of the local variables – and compare native discovery with git rev-parse for both questions. A case that expects git to refuse first checks that it does, so a fixture that accidentally built a valid repository cannot pass as a refusal. They skip only when git is absent (the Nix sandbox) or older than a feature they need. With git 2.55 on macOS none skip; with Debian’s git 2.39 on Linux (the golang:*-bookworm builder image, as a non-root user) 52 pass and the two needing 2.45 and 2.48 skip. That run found the one portability gap so far: git 2.39 counts GIT_INTERNAL_SUPER_PREFIX as repository-local and newer git does not, so the scrub list is the union.

Without git. TestScopesResolveWithoutGit and three siblings clear PATH and build repositories by hand – HEAD, objects/, refs/, and the two files git worktree add writes – so they run in the Nix sandbox too. The e2e TestRepoAndWorktreeScopesResolveWithNoGitInstalled does the same through the real binary and a live daemon, with call and exec, and reads the keys from mcpx status and the answer from mcpx doctor.

Falsified. Every test that asserts new behaviour was run against code without it:

  • The three e2e tests, against the unchanged origin/main: all fail, for the reasons they name (keys are cwd: without git; every caller keyed to other/.git under an absolute GIT_DIR; the shared instance gone after an exec).
  • The unit tests, by mutation: 44 single-line changes to the port – the walk order, the ceiling arithmetic, each gitfile refusal, each HEAD rule, has_common, the config parser’s quoting, trimming and subsections, the format checks, the environment scrub – each run against the suite on the final code. 41 are killed, each by a named test. The three that survive cannot change behaviour: two treat relative GIT_CEILING_DIRECTORIES entries differently, but a relative entry can never be a prefix of the absolute path discovery walks; the third reads the fallback’s second line even when git fails, and git prints no second line when it fails. Five mutations first written so that they did not compile were rewritten, since a compile error proves nothing about a test. The first runs found five gaps, now covered: the order of the .git and bare checks at one level, a section whose subsection is core, HEAD values that are almost valid, a .git that cannot be stat’d, and a .git file over the size ceiling.

In the image. §3 of docs/container.md, “No git”.

5. Found along the way

  • A sessionless mcpx exec stopped every shared instance it touched. CallContext.CallerOwned returned true for any key when the session was ephemeral, so the release at the end of a run stopped the global, repo and worktree instances too – contradicting ReleaseCaller’s own comment. Fixed in scope.go: an ephemeral caller owns its session’s key and its pid’s key, nothing shared. TestOnlyKeysTheCallerMintedAreCallerOwned, e2e TestASessionlessExecLeavesASharedInstanceRunning.
  • The degraded-scope log line says “per-call” when the key is the directory: #191. Not fixed here; internal/daemon/registry.go is another area’s.
  • Debian bookworm’s git (2.39) refuses a checkout made by a current git that used worktree.useRelativePaths: unknown repository extension found: relativeworktrees. Native discovery reads it, so the image is now more capable than the git it used to ship.

The home-page example: copy it or run it in the page

The example on the home page is not a picture of code. It is the working script from home-example/example.ts, and a reader can do two things with it before installing anything is required of them — and a third once it is.

Copy as mcpx exec. One click puts a command on the clipboard that runs the example against your own daemon and your own servers:

mcpx exec "$(cat <<'MCPX_EOF'
const echoed = await tools.demo.echo({ message: "hello from a script" });

console.log("demo.echo:", String(echoed));
MCPX_EOF
)"

The heredoc is quoted, which is the whole trick: an example full of apostrophes, $ signs and backticks arrives at mcpx exec byte for byte, because mcpx exec takes its source as an argument and its stdin belongs to the script. Copy as script puts the source itself on the clipboard instead, for a file under .mcpx/scripts/ and mcpx run.

Run it in the page. The example is editable, and Run executes the edited version in the browser tab. This is a simulation and the page labels it as one: the script runs through the real generated client, but its POST /v1/call is answered by a fake daemon in the page whose only namespace is a mock demo server. It is the right tool for “what does this code do” — change a line, see the output change — and the wrong one for “what will this do against my servers”, which is what the copy button is for.

What the simulation does not have: real MCP servers, the launcher, and the runtime APIs (Deno.*, node:fs) a real run provides. Running an example against your own daemon from the page is a later, opt-in stage; running strangers’ code on a hosted daemon is not planned at all. The component, its mocks, and its regeneration script live in home-example/.

The /v1 HTTP API

/v1 is the daemon’s own API — the one surface everything else sits on. The CLI talks to the daemon over it, the plugin’s client is an HTTP client for it, and the MCP tools are generated from the same declarations it is. This page is the prose around the specification: how to reach the API, the few conventions a caller has to know, and how to get a client for it in your language. The specification itself is GET /v1/openapi.json, and it is generated — see below for why that sentence does real work.

/v1

One table, one specification

Every /v1 operation is declared once, as an Op in the table in internal/api/ops*.go — 81 operations at the time of writing; the generated table in parity.md is the live count and the live list. From that declaration come the route, the mcpx_<op> MCP tool, a CLI command and the operation’s entry in the OpenAPI document. internal/api/openapi.go builds the document from the table, so GET /v1/openapi.json describes the daemon that serves it, including one path per tool in the live catalogue.

Because the document is generated from declarations compiled into the binary, it can be produced with no daemon running: mcpx openapi prints it. The two documents overlap but neither is a superset of the other. Every path they share is identical, byte for byte, and mcpx openapi adds the MCP endpoint, a REST path per mcpx tool (POST /v1/tools/{tool} is itself an operation in the table) and the settings index. The daemon’s document instead adds one concrete path per tool in the live catalogue (POST /v1/call/<namespace>/<tool>, with that tool’s input schema, generated per request by handleOpenAPI in internal/daemon/ops.go), so a server that appears after startup is described without a restart. A third form, GET /v1/openapi.json?static=1, returns the daemon document without the live catalogue merged in – only the ops-table paths – so a docs build can publish the API surface without publishing the builder’s configured servers (#1883 item 4); the offline document reads no configured servers and covers upstream tools only through the declared /v1/call/{server}/{tool} template. The two documents that used to share this name — one of them describing a server that had been removed — are recorded in the header of internal/cli/openapi.go, so the confusion cannot quietly return.

Which OpenAPI document do you want? GET /v1/openapi.json describes the daemon as it is running on this machine: its operations, its routes, its settings, and the live catalogue’s per-tool paths. The /v1 half of mcpx openapi describes the same daemon surface as compiled into the binary, without the live catalogue. mcpx schema --format openapi describes the tools of your configured MCP servers, as operations for a client generator. Feed the wrong one to a generator and you get a working client for the wrong thing.

The rendered reference page and the protocol revision matrix are generated content, and generated content is never hand-copied into a prose page — it would be wrong within two releases. The generation that embeds them in this page set is still in progress; until it lands, the interim sources are the documents themselves: the operation table rendered in parity.md (itself generated, and held current by a test) and the revision matrix in protocol.md §1, which defers to GET /v1/protocol. Both are named here, on the page, so a reader always knows which copy they are trusting.

Reaching the API

The daemon listens in two places at once:

  • The unix socket in its state directory (paths.state). This is the primary listener; the socket’s file permissions are the access control.

    curl --unix-socket <sock> http://mcpx/v1/health
    

    The host name is a placeholder — over a unix socket it names nothing; the socket is the address. The OpenAPI document’s first servers entry (http://mcpx) is written for exactly this call.

  • Loopback TCP, on daemon.port. The default port is 0, which means the daemon chooses one at start-up; GET /v1/health reports the port this daemon chose, and the daemon writes its socket, endpoint and port into its info files in the state directory for clients to discover. A named endpoint (MCPX_ENDPOINT, as unix:///path or http://host:port) is how a program pins one daemon instead of discovering whichever is running.

Every endpoint is unauthenticated. That is a deliberate local-API posture, not an oversight: binding the TCP listener wider than loopback is a deliberate act, and the responsibility of whatever does it, because whatever can route to the port can run tools as you. The same listeners mount the MCP surface at /mcp — one listener, one access-control decision.

Call-scoped settings: the X-Mcpx-Settings header

A daemon is started once and serves every later command, so a flag on one command that governs something the daemon does would be lost — accepted, validated, and ignored — if the client did not send the effective value with the request. That is what the X-Mcpx-Settings header is: a JSON object of setting path to value, carried on one request:

curl --unix-socket <sock> 'http://mcpx/v1/search?q=screenshot' \
  -H 'X-Mcpx-Settings: {"search.limit": "5"}'

Only call-scoped settings are honoured; a path the registry declares at any other scope is ignored, because a header that could reconfigure a shared daemon for everybody is not a per-call preference. A malformed header is also ignored rather than refused — the client’s bug should not cost it the call itself. The setting names and scopes are the registry’s, generated into configuration.md.

The event stream: GET /v1/events

GET /v1/events is a server-sent events stream over the daemon’s event bus — elicitations waiting for an answer, server state changes, resource updates, progress and log messages. Query parameters select what to hear: kinds= (prefixes; empty means everything), session=, server=, and uri= for resource updates on named resources (naming one subscribes to it upstream for as long as the stream is open).

Reconnects resume. ?since=N replays everything after sequence N, and the Last-Event-ID header a browser’s EventSource sends on reconnect is honoured the same way; if the retained history no longer reaches back that far, the stream says so in-band with a gap event whose payload is {"since": N, "latest": M} — since is the cursor the subscriber asked to resume from and latest is the newest sequence the bus has published, not the oldest sequence still retained — so a subscriber knows it missed everything between its cursor and the retained history and can resynchronise instead of trusting a stream with a hole in it. Over MCP, the same stream is what subscriptions/listen carries — see mcp.md.

Admin operations

An operation whose declaration carries Admin changes the daemon rather than reading from it — stopping it, restarting a server, setting configuration. In the OpenAPI document these are marked x-mcpx-admin, which is how a scoped token, when one exists, would know what to grant separately. Today they are offered rather than hidden, MCP annotations (readOnlyHint, destructiveHint) included: a caller that can reach the socket can already do these things, so withholding them would buy no safety. 20 of the 81 operations are admin at the time of writing; parity.md lists which.

Request body limits

Bodies are bounded, per surface, by settings: http.bodyLimit (default 1 MiB) for ordinary requests, http.callBodyLimit (default 64 MiB) for tool-call payloads, which legitimately carry large arguments, and http.controlBodyLimit (default 64 KiB) for the control endpoints. A body over its limit is refused by the decoder rather than truncated into a half-parsed request.

A /v1 SDK

There is no published /v1 SDK, in any language. What follows is the recommended way to get one anyway, and the one client that does exist.

The specification is generated from the ops table, so a client generated from it cannot drift from the daemon the way a hand-written SDK would — a hand-written SDK is a second copy of the table, and keeping it correct is exactly the failure the table exists to prevent. Generate with the standard tool for your language:

languagetool
TypeScriptopenapi-typescript (types) with openapi-fetch (client)
Gooapi-codegen
Pythonopenapi-python-client
most othersopenapi-generator

The flow is the same everywhere:

  1. Take the spec from the daemon you will talk to: curl --unix-socket <sock> http://mcpx/v1/openapi.json -o openapi.json (mcpx openapi works offline and every path it shares with the daemon document is the same bytes, but it has no live catalogue: a generated client that needs the concrete /v1/call/<namespace>/<tool> paths must be generated from the daemon’s own document, not the offline one).
  2. Run your generator over openapi.json.
  3. Point the generated client at the daemon — which is the one mcpx-specific step, next.

The unix-socket caveat. Generated clients assume a TCP base URL. The daemon’s primary listener is a unix socket, so pick one: run the daemon on a known loopback port (daemon.port, or start it with --port) and use http://127.0.0.1:<port> as the base URL; pin MCPX_ENDPOINT the same way for programs that read it; or give the generated client a fetch/transport that can dial a unix socket — the standard clients all accept a custom fetch or HTTP transport, which is where a socket dialer belongs. What does not work is handing a generated client the socket path as a URL and hoping.

The client that exists: DaemonClient

For harness plugins and Bun/TypeScript programs there is the plugin’s DaemonClient (plugin/opencode/mcpx/daemon.ts): a connection-reusing client that knows the discovery ladder (a named endpoint first, then the daemon info files, then health checks), speaks to the socket through Bun’s fetch unix option, and reaches every operation through its generated ops methods (plugin/opencode/mcpx/ops.gen.ts, generated from the same table), with named convenience methods beside them for the common calls. It is tested against a real daemon. It is also not published as a package — using it today means taking it from the plugin — and it is Bun-first, which is why it is the harness answer rather than the general one. The general answer is generation, above.

What is not promised

No SDK-generation check runs in CI yet: no job generates a client from the current spec and compiles it, so the generation flow above is documented and follows the spec’s own generation, but it is not yet held by a test the way parity.md is. That check belongs with the docs-generation work this page set leans on, and it is recorded as the named remainder on the parent issue rather than claimed here.

Ideas

Unvetted. Everything from a brainstorm lands here first, numbered, so nothing is lost between thinking of it and deciding about it. Items graduate to proposals.md once they have a shape, and from there to the feature list in story.md once built.

Nothing here is committed to. Some of it is probably wrong.

Format

Each item keeps the thinking, not a summary of it. A one-line version of “maybe support a list of dirs to add to the priority list, and allow either front or back, and maybe aliasing to dodge conflicts” loses the two open questions inside it, which are the part worth keeping.

So: a heading, the original thought in enough detail to reconstruct the intent, and where it went. → proposal means it has a written design. → shipped means it is built. → answered means it was a question, and the answer is recorded here, because an answer that only exists in a conversation is lost.

Should this be issues instead?

Probably, eventually. A file is right while there is one author and no repository to file against: it diffs, it reviews, and it cannot rot in a tracker nobody opens. The moment a second person is involved, or an item needs discussion threaded against it, these become issues and this file becomes an index of links. The graduation path is the same either way.


Intake 2026-09-27

Extracted from one message, grouped, not prioritised.


Paths and identity

1. Git version floor. Does --path-format=absolute need a new git? → answered. It landed in git 2.31, March 2021. The stated policy is that anything released before 2026-01-01 is fair game, so it is now used directly, with a hand-absolutising fallback kept for older git. Cost of the fallback is one extra git rev-parse only when the first attempt fails.

2. Is --git-common-dir relative to cwd or to the repo root? → answered. To cwd. Verified: from pkgs/mcpx/internal it returns ../../../.git. This matters because joining with cwd is therefore correct, and joining with the repo root would have been wrong. It is also why --show-toplevel needs no treatment: it is already absolute.

3. Is there a realpath that does not involve cwd? → answered. realpath(1) exists as a shell command, and Go’s filepath.EvalSymlinks is the in-process equivalent. Neither needs cwd for an absolute path; both resolve a relative one against cwd. So the order is forced: absolutise first, resolve second. Doing it the other way silently resolves against the wrong directory.

4. Would --absolute-git-dir work instead? → answered: no, for either scope. It returns the git directory, and for a linked worktree that is the worktree’s private .git/worktrees/<name>, not the shared common dir. Using it for repo scope would make every worktree look like a separate repository — the exact bug the scope exists to avoid. And for worktree scope the wanted value is the working tree root, not a git dir at all, so --show-toplevel is already the right call.

5. Prefer realpath everywhere for comparison and search. → shipped. Every path that becomes a key goes through one canonical() helper: absolutise, then EvalSymlinks, falling back to Clean when the path cannot be resolved. /tmp and /private/tmp now key identically, as do symlinked checkouts.

6. Are there cases where the relative form should win? Display, plausibly — repo:../.. is unreadable but repo:/Users/.../nix is long. Never identity. Open.


7. Implement round-robin budgeting. → shipped as mcpx catalog.

8. Keep whole-namespace types too, or force the budget on everyone? → answered: keep both. They answer different questions. catalog is “what exists, within a budget”; types is “I have chosen this namespace, give me all of it”. Collapsing them would make the common case need a flag, and would ration an agent that already knows what it wants.

9. How are signatures ranked — is there similarity indexing? An agent? A library? → answered: none of those. Catalogue ranking is by cost, len(line)/4, ties broken by path. No embeddings, no model, no dependency. That is deliberately different from mcpx search, which is relevance-ranked by substring scoring. Two rankings because they answer two questions.

10. Could a query bias what fits? → shipped as --bias, which promotes matching signatures ahead of the pure cost order. This is the merge of the two ranking systems and it seems to work: at a budget too small for take_screenshot, --bias screenshot promotes it anyway.

11. ls does not search — is that a separate thing? → answered: yes. ls enumerates namespaces and counts, never tools. search ranks tools across namespaces. catalog sits between them: every namespace, some tools.

12. Better search: typo tolerance, concept similarity, tags. → proposal. Worth noting the ordering: tags are cheap and exact, typo tolerance is cheap and fuzzy, similarity is expensive and needs a model or an index.

13. Types improvements beyond single-tool output. → proposal.


Protocols beyond MCP

14. Import an OpenAPI service as a namespace. → proposal. Confirmed feasible: the codegen already consumes JSON Schema, which is what an OpenAPI operation carries.

15. Emit OpenAPI per namespace. The inverse, so non-agent consumers can use the same servers without speaking MCP. → proposal.

16. Expose mcpx itself as an MCP server and/or an OpenAPI endpoint, optionally secured, with or without the MCP type wrappers. → proposal. This is distinct from 15: it makes mcpx a server, not just a client.

17. A common adapter interface so MCP, OpenAPI and anything later plug in the same way. → proposal.

18. Steal from MCP alternatives and mcp++ efforts. → proposal.


MCP server mechanics

19. How are Chrome debugging ports handled? → answered: they are not, and do not need to be. chrome-devtools-mcp has no listen-port flag. With --isolated it launches its own Chrome with a temporary profile and an ephemeral debugging port of Chrome’s own choosing. This is why mcpx has no port machinery, and why the predecessor’s auto-port code was solving a problem this server does not have.

20. Can you attach to different Chrome processes? → answered: yes, via --browserUrl http://127.0.0.1:9222 or --wsEndpoint ws://.... That is the case where per-instance templating becomes necessary, because each instance needs a different URL.

21. Auto-increment or random ports as a general feature. → proposal, as argument templating plus an allocator. Generalises beyond Chrome to any server that does take a port.

22. MCP servers have their own config — how is that handled? → answered: argv and environment, and that is all there is. MCP defines no configuration channel, so a server’s options are its command-line flags and its environment. mcpx passes command, args, env and cwd per server. The one thing MCP does define that mcpx was ignoring is instructions from initialize — now surfaced.

23. --pageIdRouting defaults to true and is documented as “useful for concurrent agent sessions”. Worth understanding before assuming pooling is the only answer to concurrency. Open.

24. What other common MCP settings are we missing? → proposal. Known gaps: sampling, roots, progress notifications, cancellation, elicitation, OAuth.


Scopes and sessions

25. Config policy for scopes — allow-list, default, and whether a flag or a script may override. → proposal.

26. Named sessions, and naming one that started anonymous. → proposal. The subtle part: an anonymous key is torn down when its caller exits, so naming it must flip that bit.

27. Per-scope keepalive defaults. → proposal.

28. Change keepalive at runtime, from CLI or script. → proposal.

29. Reap on an HTTP status code. → proposal, but MCP’s own ping is the better first version: no configuration and works for every server.

30. Reap on a webhook. → proposal. Splits into outbound (easy, it is events with an HTTP sink) and inbound (needs authentication; mcpx session stop already covers anything local).

31. Tie an instance to a pid. → shipped as scope: pid, reaped on the reaper tick when the process is gone rather than waiting out an idle timer.


Config and script discovery

32. Extra directories on the priority list, with a choice of front or back: --override-paths and --fallback-paths taking lists of directories or files. → proposal. The open question inside it is whether “front or back” is two flags or one flag plus an ordering rule.

33. Aliasing a file to a different name to dodge conflicts. → proposal, and flagged there as probably the wrong shape — shadowing already resolves conflicts by precedence, and an alias adds a second mechanism for the same job.

34. Do config and scripts share a directory? → answered: yes. .config/mcpx/config.json and .config/mcpx/scripts/*.ts, with .mcpx/ as the lower-precedence alternative spelling. Both are searched upward from cwd, and a directory carrying both spellings warns once.


Logging and output

35-43. Every command takes a display format; everything expressible as JSON is available as JSON; a logging helper for scripts accepting either a plain string or structured values; plain strings wrapped into the same record shape; automatic enrichment with ambient context; formats json / json-pretty / logfmt / text / compact / bare; message templates preserved next to the interpolated message; a default closer to a classic log line.

→ consolidated into one proposal. Answer to “does the existing logger interpolate”: there is no logger yet. The daemon prints unstructured lines and scripts have no helper at all.


Logging, round two

69. How does a script return several times? → answered: emit(), a separate export, not a log level. Logs and results share one wire channel with a kind discriminator so they stay ordered, but they are different functions with different destinations: logs go to stderr rendering, results to stdout or the envelope.

70. Should the discriminator be _kind? → answered: it is not a user-facing attribute at all. kind lives at the top of the wire record; user attributes are nested under attrs. A script writing log.info("x", {kind: "y"}) sets an attribute, not the discriminator. The real collision was elsewhere – see “Reserved attribute names”.

71. Should there be log.output() / log.return() helpers? Deliberately not. Logging and returning are different acts, and putting them on one object invites log.return being read as “log that we returned”. emit is the verb.

72. Can JS attributes merge onto Go context? → proposal, “Log context and enrichment layering”. slog does have the seam (Handler.Handle takes a context) and mcpx does not use it yet.

73. Does a streamed result also produce a log line? → answered: no. Verified: emit({a:1}) produces one stdout line and no record.

74. How does variadic translate between JS and Go? → answered: it does not, and should not. JS has real variadics, so log.info(msg, ...rest) with runtime type inspection is idiomatic. Go’s slog uses alternating key/value pairs (slog.Info("msg", "k", v)) or typed slog.Attr. Forcing either into the other’s shape would make one side unidiomatic. They meet at the wire format, which is a message plus a map, and each side reaches that in its own way.

75. How expensive is the Error-based source capture? → answered, measured: new Error().stack costs 4.89 us against a 0.10 us baseline for the JSON serialisation alone – roughly fifty times the cost of the record it decorates. Parsing the frame adds only 0.3 us on top. So capture is opt-in, and the expensive part is building the stack, not reading it.

76. Does JS run for every log message even when filtered? → answered: yes, partly, and that is worth fixing. The script always serialises and writes; the level threshold is applied in Go. A debug call in a hot loop therefore costs a write even when nothing will print. → proposal.

77. Should source capture be configurable globally and per server? → shipped for global (logging.source in config, plus the flag and the environment variable), and per-server logLevel now resolves. Per-server source is not meaningful, because source capture describes the script, not the server it is talking to.

Harness integration and distribution

44-53. Plugins for opencode 1 and 2, Claude, Codex; the split between an instructions file, plugin-injected instructions, and a tool surface; a plugin suppressing a redundant instructions file by detecting a marker; an install command per harness; hash-matching so an update never clobbers a user’s edits; daemon-side config updates; a curl-able install document; a curl-able setup document written for an agent rather than a human; mcpx setup-prompt / install-prompt; platform variants for nixos, flakes, darwin, darwin+homebrew.

→ consolidated into one proposal. The genuinely novel piece is the agent-targeted setup document: the artefact is a prompt, not a script.


Packaging and service

54-59. NixOS module and service; Homebrew formula; a non-flake nix installer; the binary installing its own service; autostart at boot versus on first use; whether sudo is ever appropriate.

→ consolidated into one proposal, with a firm view on the last one: never sudo. The daemon is per-user and owns per-user MCP servers; a system unit would run them as the wrong user and share state across users.


Transport, scale and language

60. What does the daemon actually speak? → answered. HTTP over two listeners: a unix socket for the CLI, and 127.0.0.1 on an OS-assigned ephemeral port for script clients, because fetch() over a unix socket is not portable across deno, bun and node. Not stdio, not named pipes, not files.

61. Is the port fixed or random? What if it is taken? → answered. Ephemeral, chosen by the OS, so there is nothing to collide with. It is published in a daemon record file that the CLI reads.

62. Can multiple daemons run? → answered: yes, already. One per config fingerprint, each with its own socket. mcpx daemons lists them.

63. Accept a URI with or without host and port (example.com, example.com:8008, :42). → proposal. Parsing is trivial; authentication is the actual blocker, because loopback currently provides trust for free.

64. Load testing. → proposal. Untested beyond 12 concurrent runs.

65. Go or Rust? → proposal, with a recommendation to stay: the workload is process supervision and JSON, where the runtime is idle most of the time.


Naming and comparison

66. mcpx, xmcp, or something else? → proposal. Short version: mcpx is fine, renaming gets more expensive weekly, and the only real argument is that mcpsh states the actual pitch.

67. What does opencode v2 have that this does not? → answered, and tracked: OAuth and elicitation for remote servers, in-sandbox search() so discovery never enters the transcript, a persistent PTY daemon, a shared background service, and OpenAPI import. mcpx is ahead on shell access, pooling and scoping, real-runtime TypeScript, and per-config isolation.

68. Track parity deliberately. → proposal.


Intake 2026-09-30

From one message about adapters, plus two language/transport requests. Filed straight to issues rather than proposals, per the graduation path above: these needed a design argument written down, not a line kept.


Adapters without hand-writing JSON

78. Can a binary become a declaration automatically, without a model? The AI-assisted version is already sketched (#114 has the --help parse plan, #83 has it as step 5), but both park it behind a large design and neither owns it. The deterministic half is shippable on its own. → #295.

The thing worth keeping from the thinking: --help is the worst regular source, not the best, and leading with it is why this looks harder than it is. Shell completion scripts are generated data structures — fish’s complete -c tool -l flag -d 'desc' is nearly a declaration already, and zsh _arguments specs carry flag, type and description in a fixed grammar. Cobra, clap and argparse all emit them. Ladder: completions → a published spec (which is paths.apis, not an adapter at all) → man → --help → --bare.

And completions need two ways in, not one. Extracting from the binary (tool completion fish, __complete) is the convenient path and only works when the program ships a generator. Accepting a completion file is the one that matters: distributions already ship completions for programs that have no generator at all — /usr/share/bash-completion/completions/*, zsh site-functions, the Homebrew and Nix prefixes — and those are frequently packager-written for old tools whose --help is the worst in the first place. It also makes the parser testable from a corpus instead of from installed binaries, lets someone fix the source and re-import rather than hand-editing generated JSON, and covers binaries mcpx cannot run at all. So extraction is sugar over the file parser; build the parser first. Best version of the idea: import searches the system completion directories by name before it ever probes a binary.

Also settled here: a generated declaration records the binary’s version, so mcpx adapter check can say “written against jq 1.7, found 1.8” instead of drifting silently. That was #83’s open question.

79. Can I just say git is an MCP that takes the subcommand and the flags? Not ideal but fast. → #295, as --bare, opt-in and loud.

Worth keeping why it is allowed at all, because #83 Approach A rejects the shell-escape version and the distinction is the whole argument: a bare adapter is an argv hole, not a shell hole. The cost is that it has no schema beyond “array of strings”, so the agent gets nothing to reason about and mcpx doctor can only check the binary exists. Hence: required description, a visible marker in mcpx adapter list, and probably a settings gate so an organisation can forbid it centrally.

The sharp edge found while writing it up: --bare on a binary that has its own argv-level exec escape is equivalent to Approach A. git -c core.pager='sh -c ...' is a shell escape mcpx cannot see. The sandbox is only as good as the wrapped program’s own argv surface, and the docs have to name the common offenders.

80. Go can exec a program so it can’t shell escape, right? → answered: yes, and it already does. Spec.Call builds an argv and runs exec.CommandContext(cctx, s.Command, full...) (internal/adapter/adapter.go:237). No /bin/sh -c anywhere in internal/adapter, so no word-splitting, globbing, ;, $(), backticks, pipes or redirection. The guarantee is real; see 79 for the one place it leaks.

81. Is there an issue for exec’ing bash, and for Python first-class? → answered: both already exist, no new issue needed. Shell and shebangs are #106 — its own notes already say “nearly just allow any shebang” and --shell||--bash||--shell python3. Python 1:1 with TypeScript is #105, framed there as “a second codegen backend and a second runner, not a second product”. Both sit under #175.


Languages and reach

82. Roc. → #296.

The reason it is not just “another interpreter”: Roc’s platform/application split means an application is pure and a platform supplies every effect it may have. That is the exact shape of an mcpx script — pure logic whose only effects are tool calls — and no other language in the runtime list models it natively. The dream version is mcpx as a Roc platform, where the capability boundary becomes the type system rather than a Deno flag set.

Why it is staged behind a codegen backend anyway: a Roc host is ordinarily Rust/Zig/C and links against the compiled app, so a Go host means cgo and a separate toolchain. Plus Roc is pre-1.0 with no compatibility promise. Ship the boring backend, keep the platform as research.

83. Iroh. → #297.

Reach a daemon by identity rather than by address — no port forwarding, no public IP, no ssh key copied. ssh (#85) solves encryption and auth and solves routing only if you can already reach the host, which is exactly the laptop-behind-NAT case.

The honest finding, kept because it is the part that would otherwise be rediscovered: iroh is Rust and mcpx is pure Go, and tsnet (Tailscale, embeddable in a Go program, mature hole-punching, real ACLs) and go-libp2p deliver most of the same property with no cgo. Where iroh genuinely wins is iroh-blobs — BLAKE3, content-addressed, resumable — which is the same shape #187 already wants for the dependency cache. If that convergence is the goal, iroh earns its sidecar; otherwise it probably does not.


The gap both of those exposed

84. Nobody knows who is calling the daemon. → #298, and the framing in the first draft was wrong.

Not a bug — it is written down three times as a deliberate choice (internal/daemon/server.go:214, internal/daemon/origin.go:13, internal/api/openapi.go:40): unauthenticated, socket file permissions are the access control, and the TCP bind host is deliberately not defaulted because “that has to be somebody’s decision rather than a default.”

The first draft called it blocking — a precondition for ssh, p2p and browser surfaces. → answered: no. There is no blocker for adding security and no blocker for not having it. An unauthenticated local daemon is a complete answer and stays one. The issue was rewritten to say so, and the auth coupling was stripped back out of the iroh (#297) and WASM (#299) issues, which should not carry it.

The rule that came out of it, worth keeping: do not mix auth with other things in issues. Auth work goes in one line — #253 (the interface) with the pure-auth issues ordered behind it — and other issues reference it without depending on it.

85. The permissive-interface trick. → #300, and this is the part worth remembering.

When a spec demands an interface mcpx does not have, the move is to build the interface and make it say yes: accept a user and a password, and reply “good job, you’re authorized.” That satisfies every conformance row asserting the endpoint exists, is well-formed and accepts a valid credential — which is most of #253’s 61 rows — and it cannot break anyone, because a daemon that authorizes everyone is exactly as secure as a daemon with no interface at all.

The rows that require an invalid credential to be refused are the ones that change behaviour and can lock you out of your own daemon. Those get their own issue (#300), ordered behind #253, blocking nothing.

The one hazard, and it is real: a permissive interface must be visibly permissive. mcpx doctor and startup should say “authorisation: permissive” so nobody ships it believing otherwise.

86. WASM as a tool source. → #299.

Distinct from #84, which is mcpx compiled to WASM. This is mcpx running a .wasm as a sandboxed tool source — a module reaches nothing until the host hands it an import, which is the inverse of internal/runner/runner.go:43’s admission that only Deno has a permission model worth the name.

What makes it cheap where 73 and 74 are expensive: wazero is a pure-Go WASM runtime with no cgo, so nix build keeps cross-compiling for free and the release matrix does not grow. The open tension is that the component model — which is what would make the capability set a per-module declaration — may require wasmtime and therefore cgo, forfeiting the advantage. That is the one piece of research gating the rest.

In name only

created:      2026-10-01T09:00:00-05:00
last-updated: 2026-10-01T12:30:00-05:00
increment:    2
status:       working
tags:         area:audit
description:  features, settings and tests that exist on paper and do
              little or nothing in practice. The curated list; the
              per-area audits carry the full detail.

Each entry is something a reader could reasonably believe works, and mostly does not. It is a list to fix from, not a list of excuses. Check an entry against the code before adding it – two seeded here from docs/spec/revision-conflicts.md were already fixed. Add to it when you find one; remove an entry only in the change that makes it true, and say so in the commit.

Status of this file: seeded from what the 2026-09-30/10-01 fix rounds found while doing other work, then extended by the 2026-10-01 audit of origin/main at 19237a0. This page stays curated: it carries the entries worth acting on, and the three area files carry every row, with the command and output or the file:line behind it.

  • audit/surface.md – commands, 173 settings, MCPX_* variables, flags, exit codes, /v1 routes against the CLI.
  • audit/spec.md – each MCP revision’s requirements against the code and against the conformance matrix, both directions.
  • audit/features.md – opencode plugin, exec/run, daemon pools, recipes/prompt/autonomy, adapters and OpenAPI, diagnostics, elicitation.

What the audit did not cover. Authorization was excluded throughout (known absent, #253). The settings sweep read all 173 statically but ran only 31 at two values, so most proto.*, upstream.*, transport.*, prompt.* and recipes.* settings are unverified either way. The twelve plugin.* settings are read by the TypeScript plugin and were not exercised from a live opencode. The spec pass read the shared-body loops in utilities_test.go, lifecycle_test.go and clientfeatures_test.go and the rest only by header, so it is a sample of ~1300 conformance cells, not a census. Each area file ends with its own “skipped” list; read it before concluding something is clean.

Read the specifications, not only the conformance suite. The suite tests what it has scenarios for: nothing for 2024-11-05, three scenarios for 2025-03-26, and it measures zero for features no fixture exposes. Passing it is not conformance. The per-revision requirements are catalogued in docs/spec/requirements.md and docs/compare/matrix-revisions.md; check each against the code, and check each conformance pass against what the spec actually asks. The audit found the matrix itself is the weak point: most conformance bodies register one test for a list of requirement ids, so an id reads “tested” whether or not the body touches it.

Legend: absent – declared, nothing behind it. minimal – does the least that lets a test or a doc say it exists. partial – works on some paths and silently not on others. untested – may work; no test would notice if it stopped.

Authorization

whatstatedetail
OAuth as a client (to remote servers)absenttype: oauth is refused before connecting. Every client-side conformance failure is this: token endpoint, metadata discovery, DPoP, client credentials. #253
Authorization on mcpx’s own /mcp and /v1absentAnyone who can reach the socket or port is trusted. The Origin/Host check stops browsers, not callers. #253
The permissive auth interfaceminimalAuthorizes everyone; exists so the shape is in place. #300
autonomy.max against a determined callerpartialHolds per request, but a socket caller can PUT a higher ceiling with persist:user and restart the daemon. Decision 0002 says so.

Questions from servers (elicitation, sampling)

whatstatedetail
Elicitation storagepartialReadable by any local user and never pruned. #255
Answers checked against the requested schemaabsentNothing validates an answer. Answering a question whose schema requires repo with {"totally_not_in_schema":123} is accepted silently, rc 0. (The other half of this entry – “does not show which server asks” – is fixed: elicit list has a SERVER column and elicit show prints from <server>.) #255
elicit answer key=value typespartialEvery value reaches the server as a string, so age=3 arrives as "3" for an integer field (cli/elicit.go:119-126). Pass JSON to avoid it. #255
Exit 75 when a call stops for inputpartialHolds for the first pending question on a server. While an earlier question for that same (shared) server is unanswered, later callers block instead of exiting 75 – exec waited the full two-minute expiry, then printed a cancel result and exited 0, which is the symptom #286 was closed for. The e2e tests all start from an empty question store, so none of them reach this. #286 (closed)
SamplingminimalDeclared to every upstream; answering needs an agent watching the question queue. No human review, no result validation, no error mapping. Deprecated in 2026-07-28; kept working only. #256, #210
Server-to-client requests across eraspartialWorks when the upstream is configured protocol: follow: a 2025-11-25 caller gets its own legacy session of a dual-era upstream, whose requests reach it as requests (spec/era-probe.md). Under the default modern a legacy caller still shares the modern session, where a dual-era server’s legacy-only push (the official suite’s test_elicitation, test_sampling) answers -32601 – the four server-all failures, until scripts/conformance.sh configures follow. The reverse direction – a 2026-07-28 upstream’s input_required reaching a legacy caller as elicitation/create – already worked, and is now tested (internal/e2e/follow_test.go).

Tasks

whatstatedetail
Task persistenceabsentThe store is in memory; tasks do not survive a daemon restart. #209
tasks/cancelminimalForces the cancelled status instead of letting the work reach its own end. #209
mcpx asking an upstream to run a call as a taskabsentAn upstream that requires a task fails. #209
taskSupport default for 2025-11-25 clientspartialA tool with none declared can still be called as a task. #209

Skills extension

mcpx declares io.modelcontextprotocol/skills (SEP-2640, 2026-07-28 only) and serves its own four skills from plugin/opencode/skills – each a SKILL.md plus whatever supporting files and subfolders sit beside it (mcpx-basics has a getting-started guide and an examples/ folder of scripts) – through skills/list, skills/get, resources/read and resources/directory/read. One part of the extension is not built and is not declared.

whatstatedetail
An upstream’s skills through skills/listabsentmcpx answers skills/list with its own skills only. An upstream that declares the extension has its skill:// files passed through as ordinary resources (under their own URIs in pass-through mode, as mcpx://<ns>/... otherwise), but its skills/list entries – the digests and frontmatter a host verifies against – are not relayed, so a host cannot load them as skills through mcpx. Relaying needs a new daemon route and a pool and client method for a request the upstream may or may not support, plus URI handling for the gateway’s mcpx:// rewrite; no fixture in the conformance suite serves skills, so nothing would test it today.
resources/directory/read (directoryRead)builtDeclared as directoryRead: true wherever the skills extension is (2026-07-28, and not under ExtrasOnly, which serves no skills). Lists the direct children of any directory inside a served skill – files with the metadata resources/list gives them, subdirectories as inode/directory – paginated like resources/list; a file, an unknown URI or anything outside the skills is -32602. It follows the files, not a fixed layout: TestDirectoryReadFollowsAnyLayout covers nested, empty and binary content, and TestDirectoryReadListsEveryDirectoryOnDisk checks every shipped directory against the disk. All six sep-2640-skills-directory checks in the server-all leg pass. Upstreams’ skill directories are not listed, for the reason in the row above.

Relay between hosts and upstreams

whatstatedetail
Log messages per callpartialA log message does not say which call it belongs to; with several calls on one upstream connection, each gets it.
Upstream log level on legacy revisionspartialPinned to info at connect, so an upstream’s debug messages never exist to relay.
Progress and mcpx’s own timeoutpartialProgress reaches the host but does not extend mcpx’s per-server call timeout.
Relay on prompts/get and resources/readabsentOnly tools/call relays progress and logs.
Client capabilities to 2025-11-25 upstreamspartialDeclared once at connection; per-request narrowing reaches 2026-07-28 upstreams only.

Commands and settings

Full table: audit/surface.md.

whatstatedetail
exec.timeout (documented default 120s, “kill a script after this long”)absentLocal exec never reads it. MCPX_EXEC_TIMEOUT=1s mcpx exec 'await new Promise(r=>setTimeout(r,3000)); console.log("late")' printed late after 3.8s, rc 0; the flag and --remote are no different. Only --timeout (default 0 = no limit) kills, and it prints nothing – rc 124, empty output. Scripts have no default limit.
Call-scoped settings on POST /v1/execabsentExecService builds its settings once under a sync.Once from the config file and environment (internal/daemon/routes_exec.go:170-185), so the X-Mcpx-Settings header is ignored, mcpx daemon --<flag> is ignored, and nothing marked hot: true is re-read. The scope model in configuration.md does not hold on the exec path.
Daemon-scoped flags given to a client commandabsentmcpx call --pool-call-timeout 1s … is accepted, validated, printed back and ignored – including when that command auto-starts the daemon. configuration.md names this as the failure the scope field exists to prevent, and help call advertises ~40 such settings. Nothing warns. #185
mcpx help <command> listing “usage, flags and settings”absent (flags)No command’s help has a flags section. help exec never mentions --timeout, --keep, --session, --export, --ns, --runtime. The settings it does list are the same 71 for init, man and call alike, with nothing saying which command acts on which. #144, #147
script.permissions: noneminimalnone, off and unsandboxed all map to deno --allow-all (internal/defaults/defaults.json:43-45). A reader picking “none” for no permissions gets all of them.
logstore.queryLimit, stats.toppartialmcpx log and mcpx stats read the store in-process with their own hard-coded defaults (100, 20); the settings are read only by GET /v1/log and GET /v1/stats. The flag is listed in help log.
tasks.ttlpartialApplied to exec tasks only; a /v1/call task still gets 600000ms.
Global flags after the commandpartial--config, --profile, --skip-default and --all-profiles are documented as global but only parse before the command: mcpx ls --profile nope → flag provided but not defined: -profile. An unknown profile before the command is accepted silently and returns the default set.
Restarting the daemon itselfabsentrestart replaces server processes; nothing restarts the daemon. #302
Natural language to scriptminimalmcpx prompt matches a recipe or drafts a script; no declared model source, no run path through the ceiling end to end. When nothing is produced it still exits 0. #294
prompt --modeabsentRenamed to --autonomy; --mode is flag provided but not defined, recorded only in decision 0002’s “Landed” note. Settings flags on prompt also only parse for autonomy, session and set.
mcpx.namespace validationpartialChecked for reserved words only. {"namespace":"1 bad-ns"} is accepted and listed.
Settings being read vs matteringguard gapinternal/settings/consumed_test.go proves a setting is read, not that reading it changes anything.

Scripts and their sandbox

Full table: audit/features.md.

whatstatedetail
Permission profiles as a boundarypartialA script that can reach the daemon can ask the daemon for more than its own profile allows: under --permissions strict, posting to /v1/exec with options:{permissions:"all"} returns 200 and writes the file. /v1/exec does not cap requested permissions against the caller’s.
artifact({path}) against a denied filepartialUnder strict, Deno.readTextFile is refused but artifact("leak.txt",{path:"…/secret.txt"}) succeeds, because the daemon does the read via x-mcpx-path. No test covers that header.
--suffix “runs last on both paths, like a finally”partialOn exec with a snippet that throws, the suffix never runs; on run with a file it does. Tests cover the file path only.
--remote parity with localpartialDrops the caller’s environment except --env, drops res.Logs so log.info output disappears, resolves profiles and runtimes from the daemon’s settings rather than the caller’s, and skips diagnose.preflight.
Generated result types from outputSchemaabsentstructured() is typed Promise<ToolResult> with type ToolResult = any; codegen never reads outputSchema. #216

Diagnostics and the log

Full table: audit/features.md.

whatstatedetail
Failed tool calls counted as failuresabsent"ok": err == nil (internal/pool/pool.go:959) and a tool returning isError is not an error there. mcpx call fake.boom exits 1 and prints “reported an error (isError)”, while the log record says "ok":true, stats calls shows ERRORS 0 / 0.0%, and stats errors says “No errors in the log yet.” Every error rate built on the log reports zero.
stats servers call countspartialSummed from server.stop records only, so they disagree with stats calls (5 vs 12 observed).
Attribution by serverpartialServers that share a pool id are all logged under one name, so calls to one appear against another.
diagnosepartialDoes not check argument types (a number for a string reports “Nothing to report”), nor unknown namespaces, nor a script name that does not exist – each exits 0 saying there is nothing to report. #111
doctor server healthpartialNever probes a server. Before refresh, only a server with a missing binary fails; a crashing one and an unreachable one both pass.

Adapters and OpenAPI

Full table: audit/features.md.

whatstatedetail
Adapter timeoutabsent"timeout":"1s" on a 5s program returned after 5.05s. The context error is never consulted and there is no process-group kill (internal/adapter/adapter.go:255-261). No test.
Adapter and API argument checkingabsentDeclared parameter type is not enforced at call time – "abc" for a number is passed through as argv. Unknown arguments are dropped silently, and a positional value starting with - is placed before the program’s own flags with no -- separator, so a caller can inject options.
OpenAPI cookie parameters, required query params and bodiesabsentCookie parameters are dropped from the schema and sent as query values; only required path parameters are checked, so a missing required q is sent anyway and an empty body goes out as Content-Length: 0.
securitySchemes / securityabsentNot referenced anywhere in internal/openapi or internal/cli; authentication is possible only through hand-written headers, and mcpx api --spec has no way to set them at all.
APIs as namespaces in scriptspartialOnly mcpx serve exposes them; in exec, typeof pets is undefined, though adapters do appear.

The opencode plugin

Full table: audit/features.md. Checked against opencode 1.18.31 with plugin types 1.15.5, by invoking the hooks under bun rather than inside a running opencode.

whatstatedetail
instructions optionabsentPushes onto output.parts; the v1 hook type is output: { system: string[] }, so the system prompt comes back empty. #217
remember: "permanent"absentSends persist: true where the daemon’s field is a string, so the write fails with cannot unmarshal bool into … .persist of type string.
until-gone remembered scopeabsentWritten to the file and never read; behaves exactly like indefinite.
MCPX_HARNESS_VERSIONabsentRead from OPENCODE_VERSION, which opencode 1.18.31 does not set.
Documented discovery rung 5 (“ask the user”) and the headless warning lineabsentdiscover skips rung 5; toast() returns early when headless and writes nothing, so the documented log line never appears.
Nothing loads mcpx-session.tsuntestedinternal/e2e/plugin_test.go:32 runs bun test plugin/opencode/mcpx/ only. That is the gap the persist type mismatch went through. #141

Specification and the conformance matrix

Full table: audit/spec.md.

whatstatedetail
mcpx as a 2024-11-05 server over HTTPpartialThere is no HTTP+SSE server transport. A 2024-11-05 client’s opening GET gets 405 and a JSON-RPC error, never an endpoint event. docs/protocol.md:51 and docs/spec/transport.md:9 advertise “2024-11-05 over stdio and Streamable HTTP”, a combination that revision does not have; in practice it is stdio only, and the matrix shows no gap.
The progress MUSTs (must increase, stop after completion, active tokens only, rate limit)fixedRelayed progress is now checked before it reaches the host (mcpclient.acceptProgress), and the server-side cells run a real upstream that reports out of order, after completion, in a burst and after the response (serverRelayedProgress in internal/conformance/utilities_test.go).
sampling-deprecated-new-impls-should-not-adopt (2026-07-28)partialCatalogued as “mcpx forwards; pass-through, not adoption”, but the daemon originates sampling for script generation (internal/daemon/consumer.go:589-603) and declares the capability to every upstream that could answer. The cited test asserts the declaration exists, so it passes on the behaviour the requirement discourages. #210, #256
Timeout-despite-progress and tasks/cancel requirementsuntestedThe shared bodies covering them never send progress and never send a task-augmented request, so they hold only because progress currently resets nothing. A change that let progress extend a timeout uncapped would pass every cited test. #209
The matrix’s “tested” column generallyguard gap102 loops register one test body for a list of requirement ids; every id in the list then reads “tested”. At least two rows point at the wrong test, one of which is genuinely covered by an uncited test elsewhere.
The lifecycle-stdio-client-shutdown-sequence gap (#203)fixed; record removed, the five cells runThe gap says StdioTransport.Close “sends SIGTERM at the same moment it closes stdin”. It does not: internal/mcpclient/stdio.go:260-275 closes stdin, waits stdinGrace, then SIGTERM, then SIGKILL after termGrace. All five cells (TestLifecycleClient for 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 and TestTransportClient/2026-07-28/transport-stdio) SKIP by default and PASS under MCPX_CONFORMANCE_RUN_GAPS=1. So five passing tests guard nothing and the matrix under-reports five client cells. Remove the gap, do not fix the code.

Install

Prerequisites, before the table: a JavaScript runtime — deno (recommended; it is the only runtime where typechecking works), bun, or node 22.6 or later — unless you install with nix or the container, which bring their own. git is recommended (without it, the repo and worktree scopes degrade to the working directory). macOS or Linux; on Windows, use WSL. When you are done: mcpx doctor.


Channels

The status column is the honest part of this page. Today, nix, a locally built container, and the plugin copy are the only channels that exist; everything else is designed but not built (that work is #1881), and this page will not pretend otherwise.

ChannelCommandStatus
nixnix run github:developing-today/code#mcpx -- ls to try it; nix profile install github:developing-today/code#mcpx to keep itAvailable — the flake builds pkgs/mcpx/package.nix and exposes it as an app; the wrapper puts git, deno, bun and node on PATH, so a nix install is complete with no other prerequisites
containerfrom the repository root: docker build -f pkgs/mcpx/Containerfile -t mcpx pkgs/mcpx (or podman), then run the image — see containerAvailable locally — the Containerfile builds a working image (bun as its runtime; deliberately no git, see container); the release workflow now builds the same image at the release version and pushes it on an mcpx/v* tag, and no tag has been cut yet
just the plugincopy pkgs/mcpx/plugin/opencode/ into ~/.config/opencode/plugin/ (the plugin README’s “Install”)Available as a manual copy — and it is not an install of mcpx: the plugin needs no mcpx binary on its machine, because it finds a daemon over the unix socket or an endpoint; it still needs a daemon running somewhere, and as copied it exposes no tools and injects no guidance until you set tools: true / MCPX_PLUGIN_TOOLS=1 (and instructions: true for the prompt)
bash installerper-user: curl -fsSL <short> | sh; system-wide: curl -fsSL <short> | sh -s -- --system (fallback: curl -fsSL <short> | sudo sh)Shipped, waiting on the first tag — pkgs/mcpx/scripts/install.sh is in this repository and CI installs a snapshot artifact with it (checksum verified, refusal of a tampered tarball proven); it downloads a GitHub release, and no mcpx/v* tag has been cut yet, so the commands above do not work until one is
agent prompta prompt you paste to your agent, with the llms.txt URLNot yet
pwsh (install.ps1)irm <short>/install.ps1 | iexBlocked — mcpx does not build for Windows today; WSL is the Windows answer
rpm / debdnf install ./mcpx-<version>.rpmNot yet
flatpak—Not recommended (decided in the release design, #1881)
pkg—Not yet — and still an open question: macOS .pkg, FreeBSD pkg, or Homebrew
docker (published)docker run --rm ghcr.io/developing-today/code/mcpx lsWired, not yet published — an mcpx/v* tag pushes ghcr.io/developing-today/code/mcpx:<version> and :latest; no tag has been cut yet, so pull the local build in the container row until one is

Nix caveats, stated plainly: the flake is a large monorepo flake, so installing one package from it fetches and evaluates a lot (the cost from a cold machine is unmeasured — an open question on #1878), and x86_64-darwin is excluded; aarch64-darwin, x86_64-linux and aarch64-linux are the supported systems.

The system-wide forms above are deliberate. Never sudo $(curl <short> | sh): that runs the installer unprivileged and then executes the installer’s stdout as root. The --system form lets the script escalate only the copy into /usr/local/bin.

Install as

Installing is only half the choice; the other half is how the daemon lives on the machine. Every row except the service is shipped today.

ModeWhat to doStatus
Just run the binaryNothing. Any command that needs the daemon starts one (daemon.autostart, default true), and an auto-started daemon exits after 4 hours idle (autostart.idleExit)Shipped
Per-userBinary in ~/.local/bin; config at ~/.config/mcpx/config.jsonConfig shipped; the installer ships and places the binary there once a release exists (#1881)
Per-hostBinary in /usr/local/bin (the installer’s --system); config at /etc/mcpx/config.json, the last stop on the config search pathConfig shipped but was undocumented until this page; the installer’s --system form places the binary once a release exists (#1881)
Local host (a daemon other machines reach)mcpx daemon --address <ip> --port <n> on that host; clients set daemon.endpoint / MCPX_DAEMON_ENDPOINT — a client with an endpoint set never autostarts a local daemon. Remember: the API is unauthenticated, the network is the access controlShipped
Persistent daemonmcpx daemon (foreground) or mcpx daemon --detached; a deliberately started daemon’s daemon.idleExit defaults to 0 — it never idle-exitsShipped
System serviceA launchd plist or systemd unit running mcpx daemon, with daemon.autostart false on clients so no stray second daemon startsProposed, not shipped — until then, the on-demand daemon above is the supported path
Fallback: no daemonSet daemon.inline true: servers run inside each command process. Slower, nothing pooled, nothing sharedShipped, last resort

Verify

mcpx doctor

doctor checks the runtime, git, the configuration chain, whether every configured server’s command is actually installed, and the daemon — and each failed check says what to do about it.

Then: the quick start.

Interfaces

Every way into mcpx is one of two kinds. The abstract interfaces are the declarations everything else is generated from: one operation table, one settings registry, one command list. The client interfaces are the surfaces code actually drives: a generated TypeScript client, the CLI, the daemon’s /v1 HTTP API, the plugin’s DaemonClient, and MCP itself.

This page is the map. The detail lives in the pages it links to: the CLI, the binary, scripts, and the generated parity table.

Abstract interfaces

mcpx declares each thing it can do once, as an operation — an Op in the table spread across internal/api/ops.go and its sibling ops_*.go files (81 operations at the time of writing; the generated table in parity.md is the live count). From that one declaration come:

  • the /v1 route on the daemon,
  • the mcpx_<op> MCP tool,
  • a generated CLI command, for operations that have one,
  • a typed plugin method on DaemonClient.ops (plugin/opencode/mcpx/ops.gen.ts, generated the same way),
  • the operation’s entry in /v1/openapi.json and in the document mcpx openapi prints — one document now, generated from the table; the stale hand-written serve-surface document it replaced is recorded in the header of internal/cli/openapi.go.
              internal/api/ops*.go
              one Op per operation
                        |
       +----------------+----------------+
       |                |                |
  /v1 route        mcpx_<op>        generated CLI
  (registered      MCP tool         command, and
   by hand,                         DaemonClient.ops
   held by test)                    method

One part of that diagram is deliberately not an arrow: the routes are not generated. They are registered by hand in the daemon, and a test (internal/api/parity_test.go) reads them back out and checks them against the table. Generation where it is safe, a test where it is not — the effect is the same: no surface can quietly drift from the declaration.

The claim the design makes is quoted word for word at the top of parity.md:

Everything the CLI can do can be done from a plugin, can be done over /v1, can be done from MCP.

What enforces it today is also listed there, because the parity document is generated from the same declarations and cannot describe a surface that does not exist. Each direction is held by a named test — every route has an operation and every operation a route; every non-streaming operation is reachable as an MCP tool; every operation is reachable from the CLI; every command the binary dispatches is declared in Commands() and every declared one runs (TestEveryDispatchedCommandIsDeclared); and the plugin’s operation methods are generated and exercised against a real daemon. Where an operation is privileged — stopping the daemon, for example — it is offered rather than hidden, and carries MCP annotations (readOnlyHint, destructiveHint) for a client that wants to scope on them; a caller who can reach the socket can already do these things, so hiding them would buy no safety.

The CLI and the plugin both sit on top of /v1: the CLI talks to the daemon over its unix socket, and the plugin’s client is an HTTP client for the same API. There is no second, private interface for insiders.

Two sibling declarations work the same way, and the interface pages lean on both:

  • Settings. Each setting is declared once in the registry with its type, default, scope and aliases, and is then reachable from the config file, an environment variable and a per-command flag without being restated. The inventory is generated into configuration.md; the environment names are generated into environment.md.
  • Commands. Commands() in internal/cli/manual.go declares every subcommand once, so help, the man page, shell completions and the generated command table in parity.md cannot disagree about what exists. See cli-commands.

Client interfaces

Every way code can drive mcpx, with the smallest useful snippet for each. Pick by who is calling: a script wants the generated client, a shell wants the CLI, a harness wants DaemonClient or MCP, and anything else wants raw /v1.

The generated TypeScript client — for scripts

mcpx client prints the client generated from your configured servers’ schemas; inside mcpx run and mcpx exec it is already installed as the globals tools, log and emit, with each namespace also a bare identifier. mcpx types shows the signatures.

const pages = await tools.chrome_devtools.list_pages({});

In a file of your own, import it instead:

import tools from "./mcpx-client.ts";

Discovery is in the same surface: scripts can call search() and describe() to find a tool and read its signature at run time rather than having every schema loaded up front. The contract a script runs under — stdout is the result, emit() streams, a default export is the entry point — is the scripts page.

The CLI — for people, shells and CI

mcpx ls
mcpx search screenshot
mcpx call chrome_devtools.take_screenshot '{}'

The CLI is a client interface, not just a person’s interface: --json makes its output one machine-readable document, and mcpx call <ns>.<tool> [json] makes a single tool call with no JavaScript runtime involved at all. The whole surface is the CLI page.

DaemonClient and connect() — for harnesses and Bun/TypeScript programs

The opencode plugin’s client (plugin/opencode/mcpx/daemon.ts) talks to the daemon over its unix socket — measured at about 0.17 ms per call against about 23 ms for spawning the binary. It is not published as a package; harness plugins vendor it, and any Bun/TypeScript program can do the same.

import { connect } from "./mcpx/daemon.ts";

const { client, discovery } = await connect({ directory });
if (client) await client.call("fff", "grep", { query: "x" }, sessionID);

connect() runs the discovery ladder (which daemon, on which socket, for this directory). The client itself offers generic raw / get / text / post / postRaw / putRaw methods, named typed conveniences (status, exec, call, logQuery, putSetting, …), and client.ops — one generated method per operation, so nothing is reachable only through a hand-built URL. The plugin’s own documentation is plugin/opencode/README.md, “Talking to the daemon directly”.

Any MCP client — for MCP hosts

A host that only speaks MCP needs no plugin and no client library: point it at the daemon’s /mcp endpoint, or have it spawn mcpx serve --transport stdio. mcpx is then simply an MCP server with a deliberately small tool surface — the operations above, reachable as mcpx_<op> tools. The MCP pages (overview, SDK, compatibility) are the sibling half of this page set; the protocol itself is specified in protocol.md.

Raw HTTP on /v1 — for curl and other languages

curl --unix-socket <sock> http://mcpx/v1/health

Every operation in the table is an HTTP route, and GET /v1/openapi.json describes them all — generated from the table, so a client generator fed with it produces a client for the daemon that actually exists. Do not confuse it with mcpx schema --format openapi, which describes the tools of your configured servers, not the daemon: feeding the wrong document to a generator produces a working client for the wrong thing.

Landing checklist

My reading of the brief, broken into items. Kept as the working list until the ship lands; each item records what was decided and why, so a disagreement has something specific to argue with.

Legend: [ ] todo, [x] done, [~] partial, [?] guessed — needs a look.


A. Console, from the script’s perspective

  • A1. The question is not “does it log” but “does a script that was written against a normal console still behave”. Arguments, arity, return, throw-parity, .name, formatting, getters, symbol keys, subclass instances, circular values, very large values. Test the surface, not the happy path.
  • A2. Deno.stdout.write and Deno.stderr.write bypass capture. This is correct — a script writing raw bytes asked for raw bytes — but it should be written down once, in the reference, not in an agent prompt.

B. The launcher

  • B1. Whole-launcher override. --launcher <string|file> replaces the template. --no-launcher means no launcher at all: the script is handed to the runtime directly. The brief asked for one bare-valued flag; it became two, for the reason in “Guesses worth reviewing” below (internal/cli/commands.go:490-493).
  • B2. Placeholders in a custom launcher: @entry, @globals, @prefix, @before, @onSuccess, @onError, @suffix, @import.
  • B3. Reference graph must be a DAG; a cycle is a generation-time error naming the path that closed it.
  • B4. Each placeholder resolves once. A repeat is an error unless allowRepeat names it.

C. String-or-file, everywhere

  • C1. Every string-shaped input (prefix, before, onSuccess, onError, suffix, launcher, prelude) accepts inline source or a path.
  • C2. Detection is by probe, not by guessing: if it resolves to an existing file, it is a file. Explicit forms @file:/@text: force it.
  • C3. Both forms set at the same level is an error. Set at different levels is ordinary precedence, no conflict.
  • C4. A directory means: concatenate its files, lexicographic order, shallow by default. Leading-number ordering must work, so 10 sorts after 9 — natural order, not raw byte order.
  • C5. Per-kind override of whether a directory is even allowed. A prefix probably should not be a directory; default that off in plumbing.

D. Unified configuration

  • D1. One schema. A setting is declared once with type, default, pretty name, short and long description, and aliases — and is then reachable from the config file, an environment variable, and a flag, without being restated.
  • D2. Flags and env vars are generated from the schema. Env is MCPX_<PATH> derived from the dotted path; aliases are extra.
  • D3. Boot order is explicit and inspectable: built-in defaults, then config files nearest-last, then environment, then flags.
  • D4. Alias conflicts: two spellings of the same setting at the same precedence is an error that names both.
  • D5. Cross-field validation up front — “X only means something when Y is Z” is caught before any work starts, not discovered mid-run.
  • D6. mcpx config --schema prints the whole surface.

E. Search paths

  • E1. Config, script and log locations are each a list of candidates with a built-in default.
  • E2. The null splice works in these lists, so a user can say ["./mine", null, "../fallback"] and have null stand for the built-in.
  • E3. A file where a directory was expected is accepted and means exactly that one file.

F. The logging store

  • F1. SQLite index over the JSONL, call-grained.
  • F2. mcpx log — query, filter, follow.
  • F3. mcpx stats — aggregate. Invent the useful ones.
  • F4. Trace-chain walk: from any record back through its parents.
  • F5. Rotation on age, total size, and line count — all configurable.
  • F6. A way to inspect the store directly, including raw SQL.

G. Validation before execution

  • G1. Resolve and check every path, file and directory up front.
  • G2. Type-check the generated program without running it, following imports. Deno can do this; record honestly where it cannot.
  • G3. A .ts and a .js of the same name is ambiguous. Prefer .ts, but make that a guard with a plumbing switch rather than a silent rule.

H. Plumbing

  • H1. A plumbing section for internals with no ordinary reason to change. Marked clearly as unsupported.
  • [~] H2. Wherever the code has a guard or a throw, ask whether it wants a plumbing switch. No use case needed; the point is not being stuck.

I. Commands and help

  • I1. Enumerate every command, with what it does.
  • I2. --help complete and accurate at every level.
  • I3. A man page.
  • I4. mcpx <string> — a bare argument that is obviously source or a script should run, not error.
  • I5. Decide what remains of the exec/run distinction.

J. Landing

  • J1. Small clean commits throughout.
  • J2. PR, then review rounds.
  • J3. Final report: what was guessed, what the alternatives were, why the guess, and what was deliberately not done.

Outcome

Everything above is built except where marked.

Guesses worth reviewing

Each of these had a real alternative. The choice is stated so it can be argued with.

--launcher takes a required value, with a separate --no-launcher. The brief asked for one flag that means “none” when given bare. Go implements optional-value flags only by treating them as boolean, and then --launcher mine.ts silently runs mine.ts as the script with no launcher. Two flags, no trap. The alternative – keeping one flag and documenting that --launcher=X needs the equals sign – trades a real misfire for a smaller surface.

Directory concatenation is off for phases by default. plumbing.sourceDirAllowed governs it, and phases pass AllowDir: true while --launcher passes false. A launcher assembled from several files is more likely a mistake than an intention; a prefix assembled from several is plausible.

defaults wins over pool. Two spellings of one block. The older one wins so that adding the synonym cannot change what an existing file means. The alternative – pool winning as the “canonical” name – would silently alter existing configurations.

The bare-argument heuristic is narrow. A first argument runs if it names a file, ends in a script extension, or contains (){};, =>, await , console. or tools.. A bare = was in that set and is not any more, because a mistyped flag has one and running a typo as a program is worse than refusing it.

Percentiles are exact, not estimated. These datasets are small enough that an estimator would only add error.

Redaction elides partial matches rather than whole values. A variable holding several pairs is still worth seeing without the one that matters.

Not done, and why

Full projection of the registry into every consumer. output.json, paths.state, paths.cache and most of daemon.* still have their own paths, and two of those must: the state and cache directories say where to look for configuration, so they cannot come from configuration. docs/story.md names exactly which settings the registry governs rather than implying it governs all of them.

A plumbing switch for every guard. Nine exist. The ones left alone are those where the other branch is not a coherent program – a malformed daemon response, a socket that will not bind. A switch there offers a choice between working and not.

mcpx exec and mcpx run remain separate. The difference is real: exec generates the module so a prefix shares scope and can declare bindings; run imports a file so a prefix can act but not declare. Merging them means picking one and losing the other. A bare argument now dispatches to whichever fits, which was the part that actually chafed.

H2 is partial by design. Nine guards got a plumbing switch. Every throw in the tree was not audited one by one; the ones that were left alone are those where the other branch is not a coherent program – a malformed daemon response, a socket that will not bind. A switch there would offer a choice between working and not.

I5, the exec/run distinction, stayed. The difference is real and small: exec generates the whole module, so a prefix shares scope and can declare bindings; run imports a file, so a prefix can act but not declare. Merging them would mean picking one of those behaviours and losing the other. A bare argument now dispatches to whichever fits, which is the part that was actually inconvenient.

mcpx level-2 page map

The map for the level-2 docs: the layer a person reads right after installing mcpx (“basically a more normal set of docs — a new person might assume this is all there is and won’t be upset”). It assigns every one of the 30 items in the source notes to a page and a fixed anchor, records the repo material each item draws on, the issue that writes it, and which parts come from the generated reference.

  • Tracker: #1898. This map is part 1 of #1848; the first seven items’ content is #1848 part 2, still to write.
  • Structure: Approach C from #1848 — about ten grouped pages, each item with its own fixed anchor (page#anchor), anchors set explicitly rather than derived from headings so a renamed heading never breaks a link. Totals: 10 pages, 30 anchors — configuration 4, modes 3, interfaces 2, cli 3, mcp 3, binary 1, scripts 1, http-api 2, plugins 6, faq 5.
  • Navigation order follows the notes. (#1848 open question 3 asks whether task order would be better; the notes’ order stands until that is answered.)
  • Writing issues, in this repo’s numbering: #1848 (configuration + modes), #1851 (interfaces, CLI, MCP, binary, script, /v1), #1855 (plugins), #1859 (FAQ). The generated reference is #1843. (#1848’s body quotes the original dezren39/nix numbering — #146/#147/#148/#149 and #144 — for the same issues.)
  • Rule for generated content at level 2. When a page needs reference data, it embeds the relevant slice generated from the same source (for example “settings where scope = client”), it does not just link to the full reference — linking would break “this is all there is”. #1843 has to support filtered embeds, not only whole pages (#1848 open question 7).

Verification (2026-10-29, HEAD 5a34fa70)

Every source path below was re-checked to exist at HEAD. Paths are relative to pkgs/mcpx/. Three corrections to the refs as quoted in #1848, found by that check:

  • The story file is docs/story.md; there is no pkgs/mcpx/story.md.
  • Line numbers quoted in #1848 have drifted (for example the mcpServers tag is now at internal/config/config.go:190 and SearchPathFrom at internal/config/config.go:713). This map cites files, not lines, so it does not rot the same way.
  • .mcpx/scripts/ does not exist at HEAD (see item 82).

The map

Notes lineItem (verbatim)Page #anchorSource material in the repoWritten byGenerated part (#1843)
53common configconfiguration#common-configdocs/configuration.md “Where a value can come from”, “Scope”, “Hot”, “Reaching them”; internal/config/config.go (search path); internal/cli/manual.go entries config, settings, init#1848link to the settings table; no copy
54server configconfiguration#server-configinternal/config/config.go (server block); docs/story.md “Sharing and scope”, “Profiles and aliases”, “Remote servers without a shim”; docs/configuration.md “Servers”; internal/cli/manual.go servers#1848open: a JSON Schema for the file (pool.* rows are generated)
55client configconfiguration#client-configdocs/configuration.md “Scope: who reads it”; registry rows with scope client and call (internal/settings/registry.go)#1848the client- and call-scoped settings, filtered from the registry
56env varsconfiguration#env-varsdocs/configuration.md “variable” column; docs/environment.md, generated from settings.ScriptEnv() (internal/settings/scriptenv.go) and held to the setters by a test; the names neither source declares, listed below#1848registry names and the script environment both generated; only the names neither declares are hand-written
57client modesmodes#client-modesdocs/story.md “Running without a daemon”, “One daemon, many clients”; docs/exec.md “Where a script runs”; AGENTS.md “Profiles”#1848—
58transport optionsmodes#transport-optionsdocs/protocol.md §5; docs/story.md “What each transport costs”, “Server-sent events, not WebSockets”, “Lossless reconnection”, “Remote servers without a shim”; docs/dependencies.md “Network”#1848—
59server modesmodes#server-modesdocs/story.md “mcpx speaks MCP”, “Sharing and scope”; docs/protocol.md §5#1848—
60abstract interfacesinterfaces#abstract-interfacesinternal/api/ops*.go; docs/parity.md#1851the parity table
61client interfacesinterfaces#client-interfacesmcpx client; internal/codegen; plugin/opencode/mcpx/daemon.ts (DaemonClient)#1851—
62clicli#cliAGENTS.md “The loop”; cmd/mcpx/main.go#1851—
63cli argumentscli#cli-argumentscmd/mcpx/main.go; internal/settings/bind.go#1851per-command registry flags
64cli commandscli#cli-commandsinternal/cli/manual.go Commands()#1851yes: the whole command table
65mcpmcp#mcpdocs/story.md “mcpx speaks MCP”; docs/protocol.md §2–§4#1851—
66mcp sdkmcp#mcp-sdkdocs/dependencies.md; go.mod#1851—
67mcp compatibilitymcp#mcp-compatibilitydocs/protocol.md §1, §6; internal/mcpserver/revisions.go; GET /v1/protocol#1851the revision matrix
68binarybinary#binarydocs/dependencies.md; package.nix#1851—
69scriptscripts#scriptAGENTS.md “Scripts”, “Script contract”; docs/story.md “The script contract”, “Named scripts”; docs/exec.md#1851—
70/v1http-api#v1GET /v1/openapi.json (internal/api/openapi.go); docs/parity.md#1851yes: rendered from the OpenAPI document
71/v1 sdkhttp-api#v1-sdkDaemonClient (plugin/opencode/mcpx/daemon.ts); /v1/openapi.json#1851—
72pluginsplugins#pluginsplugin/opencode/README.md#1855—
73plugin compatibilityplugins#plugin-compatibilitydocs/opencode-plugin.md §1, §5#1855—
74plugin interface & configplugins#plugin-interface-and-configplugin/opencode/README.md “Configuration”; docs/plugin-discovery.md; plugin.* registry rows#1855plugin.* rows
75opencode v1 pluginplugins#opencode-v1-pluginplugin/opencode/README.md; docs/opencode-plugin.md §2, §3, §6#1855—
76opencode v2 pluginplugins#opencode-v2-plugindocs/opencode-plugin.md §4#1855—
77vibeMcpplugins#vibemcp (provisional; moves once its meaning is known)nothing in the repo#1855—
78“so why’d you get rid of all the mcp and then make one, do you like mcp or not”faq#do-you-like-mcpdocs/story.md “# Summary”, “mcpx speaks MCP”#1859—
79“i mean, opencodemode is right there”faq#opencode-code-modeOPENCODE-V2.md (with its 2026-09-29 corrections); docs/opencode-plugin.md §1#1859—
80“is it safe?”, no.faq#is-it-safeAGENTS.md “Scripts”; internal/runner/runner.go#1859—
81“but why would you..”faq#whyASSESSMENT.md; docs/story.md opening walk#1859—
82“but can i get some examples?”faq#examplesdocs/story.md walk; mcpx recipes (internal/cli/manual.go); Examples fields in internal/cli/manual.go. ⚠ .mcpx/scripts/, cited in #1848, does not exist at HEAD — do not cite it as a source until it does#1859per-command examples

#1855 may split plugins into an index plus an opencode page. The anchors stay the same either way.

Contradiction flags — do not copy these into any page

#1848’s Findings name three places where the current docs or behaviour contradict each other. They are flagged here on the pages they touch, with their state re-verified at HEAD 5a34fa70 (2026-10-29):

  1. The file key is mcpServers, not servers — STILL TRUE. Config.MCPServers is json:"mcpServers" (internal/config/config.go), the same object other MCP hosts use; “servers” is the CLI command and the /v1 resource. Touches: configuration#common-config, configuration#server-config. Both pages must show both names and say which one goes where.
  2. mcpx init’s starter config taught a dead mode key — RESOLVED at HEAD; do not reintroduce it. When #1848 was written, starterConfig set "mode": "shared" / "mode": "session", keys the loader silently drops (mode had split into sharing and scope). At HEAD the starter config teaches sharing/scope correctly and a test pins it (TestStarterConfigMeansWhatItSays, internal/cli/commands.go), and the top-level help’s CONCURRENCY block (internal/cli/root.go) teaches sharing/scope too. Touches: configuration#common-config, configuration#server-config, modes#client-modes, modes#server-modes. Pages teach sharing/scope; the mode key and its pooled value must appear nowhere.
  3. The config search path is wider than anything documented — STILL TRUE. SearchPathFrom (internal/config/config.go): MCPX_CONFIG alone if set; otherwise .config/mcpx/config.json, .mcpx.json, .mcpx/config.json and .mcp.json at each directory from the working directory up to /, then $XDG_CONFIG_HOME/mcpx/config.json, ~/.config/mcpx/config.json, ~/.mcpx.json and /etc/mcpx/config.json. .mcp.json is Claude Code’s project file, so mcpx picks up a project’s Claude Code servers — nothing says so in the current docs. Touches: configuration#common-config (the page must list the path exactly, in order).

Two smaller points from #1848 that pages must also get right:

  • Env vars outside the registry (touches configuration#env-vars): the registry is not the only declaration there is. settings.ScriptEnv() (internal/settings/scriptenv.go) declares the environment mcpx and the plugin hand to a script — MCPX_SESSION, MCPX_SESSION_ID, MCPX_PARENT_SESSION_ID, MCPX_EPHEMERAL, MCPX_RUN, MCPX_INPUT, MCPX_ENDPOINT, MCPX_SOCKET, MCPX_ARTIFACTS_LOCAL, MCPX_CWD, MCPX_PID, MCPX_ENTRY, MCPX_CLIENT, MCPX_CONFIG_PATH, MCPX_SCRIPT_DIRS, MCPX_RUNTIME — and docs/environment.md is generated from it, with a test holding the two directions together (scriptenv_test.go). So the caller identity the plugin sets (plugin/opencode/mcpx-session.ts) and the names the generated client reads (internal/codegen/emit.go) are already declared, not hand-written. What an env-var page must add beyond the two generators is the small remainder neither declares — config selection (MCPX_CONFIG) and harness context — and it should link the generated page rather than copy it.
  • Upstream “sse” is not the legacy SSE transport (touches modes#transport-options): POST /v1/servers accepts transport: stdio|http|sse (internal/api/ops_settings.go), but the pool builds only a stdio transport or Streamable HTTP (internal/pool/pool.go, internal/mcpclient/http.go) whose replies may arrive as an SSE stream. Separately, the deprecated 2024-11-05 HTTP+SSE transport does have a client — LegacySSETransport (NewLegacySSE, internal/mcpclient/sse.go) — so the transport page must document that fallback rather than deny it, and say which paths reach it, while still not implying that transport: sse means an SSE-only server (#1848 open question 5).

MCP

mcpx speaks the Model Context Protocol in both directions, and in both eras of it. As a client it connects to every MCP server in its configuration, so their tools are reachable without their schemas ever entering a model’s context. As a server it offers itself — a small gateway surface over all of those servers — to any MCP host. The protocol reference is protocol.md; this page is the map of the surface a host or a client connects to.

MCP

mcpx as an MCP server

A host reaches mcpx’s MCP surface two ways:

  • Streamable HTTP, at the daemon’s /mcp. The daemon mounts /mcp on the same listeners as its /v1 API: the unix socket and the loopback TCP port. There is no separate MCP server process and no serve --transport http — that command was removed, and asking for it fails with the address to use instead (protocol.md §5 tells the whole story). To put MCP on a network, move the daemon’s listener: mcpx daemon --port 8899 --address 0.0.0.0, then connect to http://host:8899/mcp. The warning that applies to /v1 applies here unchanged: the surface is unauthenticated, so whatever can route to the port can run tools as you.

  • stdio, via mcpx serve. An MCP host starts its servers by spawning a process and talking down the pipe, so something has to be spawnable; serve is that something, and it is a thin shim — every tool it lists is answered by the daemon, which it starts if it is not running. A host configuration looks like the one every host already knows:

    { "mcpServers": { "mcpx": { "command": "mcpx", "args": ["serve"] } } }
    

The surface is small on purpose. Exposing every upstream tool over MCP would rebuild the problem mcpx was built to solve — hundreds of schemas in the model’s context — with extra steps (story.md, “mcpx speaks MCP”). What a host gets by default (serve.mode = code-mode) is the gateway: the named tools (mcpx_exec to run code next to the servers, mcpx_call for one direct call, mcpx_search / mcpx_types / mcpx_catalog for discovery) plus a generated mcpx_<op> tool for every operation in the /v1 table — every one of them a way to reach the upstreams through mcpx, never the upstreams’ own schemas pasted into the list. Prompts are offered as <namespace>_<prompt> and resources as mcpx://<namespace>/<uri>. Two more modes widen it: reactive keeps a bounded working set of upstream tools, adapting as calls name new ones, and a pass-through server (mcp.passthrough, --passthrough <server> on mcpx serve or mcpx daemon) is offered as itself — its tools, prompts and resources under their own names, its results verbatim.

A client can also ask for the raw surface per connection: a request to /mcp?codemode=false gets every upstream tool, flat, under a namespaced name (<namespace>_<tool>), and the gateway’s own tools are not on that list — the raw surface replaces the code-mode surface, it does not add to it. Calls to a raw name go to its upstream under the upstream’s own name. This is the switch opencode v2 appends to a remote server’s URL when the server is not configured codemode: false, so a v2 host’s own code mode can orchestrate mcpx’s pooled servers directly. Any other value, and the parameter’s absence, change nothing; the switch is read from each request’s URL, so one connection can be answered raw and the next through the gateway.

Questions flow backwards over the same connection. When an upstream tool asks something mid-call — an elicitation, a sampling request — mcpx puts the question to the client that made the call when that client said it could answer, and to its broker otherwise (elicitation.md). And subscriptions/listen, the modern subscription stream, is accepted from a client of any era, each notification tagged with the subscription id that asked for it.

Which methods are answered, per revision, and which result shapes are downgraded for older clients, is protocol.md §2 — generated claims are not repeated here because that document is where they are held to the code.

mcpx as an MCP client

Upstream, mcpx is a dual-era client of every configured server. It probes the modern era first (server/discover), falls back to the legacy handshake, and caches the answer per server configuration, so the probe costs one round trip rather than one per start. To a legacy server it offers 2025-11-25 and accepts anything from 2024-11-05 up; a per-server override exists (upstream.protocol) for a server that needs to be met on specific terms. Transports are stdio and Streamable HTTP, with the 2024-11-05 HTTP+SSE transport as the last fallback of the HTTP probe. The per-revision client rules are protocol.md §4.

The server-initiated requests mcpx answers for its upstreams:

requesthow mcpx answers
ping{}
roots/listthe daemon’s configured roots
elicitation/createthrough the broker — the question is routed to whoever can answer it, a host client included
sampling/createMessagethrough the broker; a decline becomes an error, because sampling has no decline result
anything elsea -32601 error — always answered, because silence is indistinguishable from a hung server

A modern server asks the same things by answering input_required instead of sending a request; mcpx resolves those through the identical function, so the two eras cannot answer the same question differently.

The MCP SDK question

“mcp sdk” names three different things. The honest answer is different for each.

1. Which MCP SDK mcpx uses: none

mcpx is built on no MCP SDK. There is no MCP module in pkgs/mcpx/go.mod; the client (internal/mcpclient), the server (internal/mcpserver) and the JSON-RPC framing are mcpx’s own code — about one package each, which is the point. dependencies.md says so in as many words, and the dependency list stays at the handful of Go libraries there because mcpx speaks the protocol itself instead of depending on somebody else’s reading of it.

2. Using mcpx from an MCP SDK: any of them

Any official SDK client connects the way it connects to any MCP server: over Streamable HTTP to the daemon’s /mcp, or by spawning mcpx serve for stdio. With the official TypeScript SDK:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({ name: "my-host", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL("http://127.0.0.1:8899/mcp")));
const tools = await client.listTools();
const status = await client.callTool({ name: "mcpx_status", arguments: {} });

(The port is the one the daemon is actually on: with the default daemon.port of 0 the daemon chooses one at start-up, and GET /v1/health reports it — see http-api.md. For stdio, swap the transport for the SDK’s StdioClientTransport with command: "mcpx", args: ["serve"].)

3. An SDK mcpx provides for writing servers: there isn’t one

mcpx publishes no SDK for writing MCP servers or mcpx extensions, in any language. What exists for building on mcpx is the surfaces themselves: a server is any program that speaks MCP and is named in the configuration, and a program that wants to drive mcpx uses the /v1 API and its generation story or the plugin’s client described there. A page that implied a supported server SDK would be promising a thing nobody maintains.

Compatibility

The revisions

mcpx serves five revisions, in two eras, and speaks the same five upstream:

revisioneramcpx servesmcpx speaks upstream
2024-11-05legacyyes, over stdio and Streamable HTTP; its HTTP+SSE server transport is not hostedwhen the server answers with it; HTTP+SSE as the last fallback
2025-03-26legacyyeswhen the server answers with it
2025-06-18legacyyeswhen the server answers with it
2025-11-25legacyyesoffered on every initialize
2026-07-28modernyesprobed first

The split that matters: a legacy implementation negotiates once and can send its peer a request at any time afterwards; a modern one has no handshake, restates its version on every request, and — as a server — has no channel on which to send a request of its own. Modern against legacy fails; only a dual-era implementation bridges them, and mcpx is dual-era in both directions.

This table is a summary, and it knows it. The matrix lives as data in internal/mcpserver/revisions.go and is served by GET /v1/protocol; protocol.md §1 defers to that endpoint for the same reason this page does — a matrix in a document and a matrix in code agree on the day they are written and never again. A generated rendering of the matrix on this page is part of the docs-generation work tracked alongside this page set; until it lands, the endpoint and §1 are the sources, named here so a reader is never left trusting a copy.

Accept liberally, send conservatively

mcpx accepts any request a client’s revision allows it to make sense of — subscriptions/listen and core tasks/* are answered for legacy clients that predate them — and sends nothing the client’s revision never defined: every result is built in the newest shape and spelled down once, at the edge, so an older client is never handed a field it was never told about. The rule stops at removal: a method a client’s revision removed is answered -32601, because the removal is the specification naming a replacement, and answering it anyway would hide which era the server is speaking.

Known gaps

The gaps are written down in protocol.md §6 rather than copied here, so they cannot drift from the fixes: no OAuth on /mcp, tool inputs and outputs not yet validated against their JSON Schemas, a tool with taskSupport: "required" that cannot be called, and the smaller conformance items listed there. A gap named in one place is a gap somebody can close.

Host compatibility

No MCP host has a recorded compatibility result yet, so this page claims none. Any host that speaks one of the five revisions over stdio or Streamable HTTP is expected to work — that is what the conformance work in protocol.md holds mcpx to — but “expected” is not “tested against your host”, and a host will be named here only when somebody has actually pointed it at mcpx and written down what happened.

Now what

You installed mcpx and ran the loop. Each link below is a question, and every one of them is answered one level down — the level-2 pages mapped in the level-2 page map.


  • Where does config go? One mcpServers object, a search path that runs from the working directory up to /etc/mcpx/config.json, and settings that can come from a file, the environment or a flag: configuration, and the map’s configuration page (common, server and client config, env vars).
  • How do I call mcpx from my harness, over MCP, over HTTP? The CLI is one surface of four — the daemon also speaks MCP (/mcp, mcpx serve) and plain HTTP (/v1/*, OpenAPI at /v1/openapi.json): interfaces, cli, and the map’s mcp and http-api pages.
  • How do scripts actually work? The script contract, named scripts, runtimes and what a script may touch: scripts, exec, runtimes.
  • How does the opencode plugin work? It finds the daemon so your harness does not have to ship a binary: plugins and the opencode plugin.
  • Something is wrong. mcpx doctor first (it says what to do about each failure), mcpx status for the daemon and its servers, and the skeptical questions, answered: FAQ.
  • Everything, in depth. The long version of all of it — the narrative the overview was distilled from, and the book it feeds: the story and the compendium.

The opencode v2 plugin — verified API facts and implementation map

Companion to opencode-plugin.md, which sketches the v1 design and the v1→v2 mapping. That document’s v2 claims came from a reading of opencode’s v2 branch; this one re-checks every fact the rewrite depends on against the pinned v2 source (reported version 2.0.3) and records where the pin differs. Work item: #1858, queue task 3068.

created:      2026-10-29T23:05:00-05:00
status:       verified facts + implementation map (core rewrite landed in PR #2489: first slice + the tools.ts slice)
scope:        CORE rewrite only (#1858 SET-REMAINDER is out of scope)

Verified against the local opencode fork checkout (~/git/opencode, main at c9edb37fda), paths below are inside packages/.


1. Loading: {id, setup} is the whole contract

core/src/plugin/module.ts:107-134 — the module schema accepts exactly two default-export shapes:

{ id: string, setup: (context: Context) => Promise<Cleanup | void> | Cleanup | void }
{ id: string, effect: (context: Context) => Effect<void> }

Only default is read (Module = Schema.Struct({ default: ... })). Failure to match aborts the module with “Plugin must export a default definition with an id and an effect or setup function.” Consequences for the port:

  • the plugin/opencode/mcpx/ subdirectory workaround is unnecessary in v2, but the v1 tree keeps its layout until v2 is what runs;
  • helpers can be plain sibling modules in the v2 tree, and only the default export is inspected — so export const TUNING, export class DaemonClient etc. are safe to import from a shared file;
  • the v2 loader accepts a plugin directory as well as a file, so the v2 tree can be one directory whose index.ts holds the default export.

2. The context surface the port actually uses

promise/plugin.ts:26-73 (the setup variant of Context):

port needv2 surface
daemon choice remembered across sessionsctx.storage.get/set/remove/scan (Promise, schema.Json values)
session id (the thing shell.env used to carry)ctx.session.hook(...) inputs carry sessionID; ctx.session.context({sessionID}) reads one
system-prompt linectx.session.hook("context", input => …) mutating input.system
opt-in toolsctx.tool.transform(editor => …), editor.namespace(...) + editor.add(...)
tool timingctx.tool.hook("execute.after", …) — a union on status
a line to the userctx.tui? no — server plugins get the SDK client.tui.* only (unchanged from v1)
headless detectionunchanged problem; no isMainThread trick in v2’s two-process model

ctx.generate (one-shot completion), ctx.mcp.transform, ctx.permission.hook("evaluate") and the TUI select() picker are the SET-REMAINDER — deliberately not used by this core.

2.1 execute.after really is a status union

promise/tool.ts:41-70: the hook input is { tool, sessionID, agent, messageID, id, input } intersected with either { status: "completed", result } or { status: "error", … }. So the timing record gets a real failure signal; the v1 tree only ever saw success.

2.2 session.hook("context") is the system-prompt path

promise/session.ts:25-37 — SessionRequest is { sessionID, model, system: Array<SystemPart>, messages, options }, and SessionContext adds { agent, tools }. Pushing a part onto input.system is the direct replacement for experimental.chat.system.transform; no per-command hook is involved. Note the hook fires per turn, so the injected paragraph must be idempotent (append once, not accumulate).

3. The session environment — two corrections to the mapping table

The mapping table on #1858 says PUT /api/experimental/session/:id/environment. Against the pin:

  1. The path is /api/session/:sessionID/environment — protocol/src/groups/session.ts:879-891. Payload is { variables: Record<string, string> }, success is 204.
  2. It replaces, it does not merge. core/src/session/environment.ts:18-31 keeps a Map<SessionID, Variables> and set() overwrites the entry; core/src/shell.ts:262 then reads environments.get(sessionID) ?? process.env.

That second point is load-bearing: a plugin that sends only MCPX_* variables would hand every shell command in that session an environment without PATH, HOME, or anything else. The v2 adapter must send the full environment it wants the session to have — { ...process.env, ...MCPX_* }, stringified — or must not set one at all. Verified by reading the consumer, not the doc comment.

The plugin reaches this route either through ctx.serverUrl ?? http://… + fetch, or (preferred, when the SDK exposes it) through the session client. The promised-domains list (promise/session.ts:153-172) does not include an environment method, so the raw HTTP call is the route the core rewrite should plan for, with a ctx.serverUrl === undefined fallback (embedded hosts: skip, degrade to “unknown session”, exactly as v1 degrades).

4. ctx.storage replaces the remembered-daemon file

promise/storage.ts — get(key) => Promise<Json | undefined>, set(key, value), remove(key), scan({ prefix }). Plugins are namespaced per plugin id by the host, so the v1 rememberPath() / readRemembered() / writeRemembered() trio (and the TUNING.rememberFile knob) map onto one key per daemon target. until-gone lifetime is preserved by remembering { target, socket, seenAt } and dropping it when the health check fails — the same rule as today, with the file gone.

5. What the port does not need to rewrite

plugin/opencode/mcpx/daemon.ts is already harness-free: discover(), DaemonClient, rank(), summarise(), readInfoFiles(), stateDir() and the health checks take an env map and a directory and speak /v1 over the unix socket with Bun’s fetch(url, { unix }). v2 keeps Bun and keeps the socket, so the v2 tree imports this file unchanged. Only the rung-4 spawn (runBin, askCLI) is harness-specific: node:child_process still works in v2’s server process, but the core rewrite should make rung 4 optional (off unless MCPX_PLUGIN_BIN names a binary) rather than assume it.

6. Implementation map (first slice)

Target tree, beside the v1 files, all under pkgs/mcpx/plugin/opencode/:

filecontentsacceptance it satisfies
index.tsexport default { id: "mcpx", setup }loads via {id, setup}
options.tsthe v1 Options type + pluginSettings()-declared variable names, ported verbatimsettings guard
session-env.tsfirst-sighting → PUT /api/session/:id/environment with the full envonce per session, no per-command shell.env
context.tssession.hook("context") appending one idempotent paragraphsystem line
tools.tstool.transform with namespace + discover/exec/observe/daemon_*namespaced registration
timing.tstool.hook("execute.after") recording both statusesfailures recorded
remember.tsctx.storage remembered daemonno state file
daemon.tsre-export of ../mcpx/daemon.tscore reuse

Tests follow the existing bun test style in plugin/opencode/mcpx/*.test.ts (daemon.test.ts, ops.test.ts, ceiling.test.ts), stubbing the context object: hooks, storage map, and a fake fetch for the environment route.

7. Open questions for the implementer

  • Namespace spelling — RESOLVED by the tools.ts slice, against the pin: editor.namespace({ name, description }) takes an object, and packages/core/src/tool/runtime.ts (effectiveName) flattens a namespaced tool to <namespace>_<name> in the model’s tool list. Registering name exec under namespace mcpx therefore yields mcpx_exec — the v1 hand-named tools survive unchanged, and the mcp.transform remainder work inherits the same flattening rule.
  • Environment route reachability — with ctx.serverUrl undefined (embedded SDK host) there is no HTTP route; decide degrade quietly vs. first-tick retry when a later hook reveals a URL.
  • context hook idempotency — whether the host hands the same system array across turns or a fresh one per request decides between “append once, guarded” and “append each turn”.
  • Settings guard scope — TestThePluginReadsNoUndeclaredSetting scans plugin/**/*.ts(x); the v2 tree must therefore read only the names in internal/settings/plugin.go, including plugin.discoveryRetry, plugin.remember and plugin.annotate.

The opencode plugin

What it is, why each part exists, what is opencode-specific, and what happens to all of it in opencode v2.

id:           the-opencode-plugin
since:        pre-release
created:      2026-09-29T17:00:00-05:00
last-updated: 2026-09-29T17:00:00-05:00
status:       proposed
note:         findings + design sketch

This document has three jobs. It records what was found comparing opencode v1 and v2 (§1–2), sketches the v1 design that follows from it (§3), and separates the parts that are about opencode from the parts that are about connecting any agent harness to mcpx (§5), because the same plugin is wanted for codex, claude, pi and manus.


1. The two opencodes

They are not two products. They are two revisions of one monorepo.

“v1”“v2”
flake inputopencode, rev e03db9bopencode2, rev d0a9028
version1.18.31 (packages/opencode/package.json:3)2.0.3 (packages/cli/package.json:4)
shipped apppackages/opencodepackages/cli, renamed opencode2

The v1 pin already contains the whole v2 codebase — packages/core, packages/cli, packages/tui are all there at 1.18.31. The v2 pin has deleted packages/opencode. So the difference is which binary is built, not which source exists.

Three corrections to OPENCODE-V2.md, which predates this:

  • Code mode is not v2-only. v1 ships packages/codemode and packages/opencode/src/tool/code-mode.ts, behind OPENCODE_EXPERIMENTAL_CODE_MODE (src/effect/runtime-flags.ts:48). With it on, MCP tools leave the model’s tool list exactly as in v2.
  • Both accept TypeScript in code mode; neither rejects it.
  • The pinned v2 reports 2.0.3, not 2.0.18.

And one thing that matters for anyone writing a plugin today: v1.18.31 runs both plugin systems at once. PluginV2.node is in the location service group (packages/core/src/location-services.ts:52), which the v1 app depends on. A well-formed v2 plugin file dropped in .opencode/plugin/ is loaded by the v2 host and also rejected by the v1 loader, which logs a complaint and carries on. Noise, not breakage.


2. What changes for a plugin

2.1 Loading

v1v2
glob{plugin,plugins}/*.{ts,js}, not recursiveplugin/plugins dirs, files or directories
accepted shapesthree: structured {id, server}, legacy “every export is a plugin”, and v2 {id, effect|setup}one: {id, setup} or {id, effect}
v1 compatibility—none. No shim, no fallback

The legacy loader is why mcpx/daemon.ts sits in a subdirectory: v1 calls every export of every file in the glob as a plugin, and one export that is not a function aborts the whole module. The glob not being recursive is what makes the subdirectory work.

In v2 that workaround becomes unnecessary — only default is read — but the plugin itself must be rewritten, because v1 plugins do not load in v2 at all.

2.2 The four hooks mcpx uses

mcpx hookv1v2
shell.env✅ carries cwd, sessionID, callID⚠️ shell.create.before carries command, cwd, timeout, shell, env — no sessionID
experimental.chat.system.transform✅✅ session.hook("context"), push onto event.system
tool: {...} map✅ un-namespaced✅ tool.transform(e => e.add(...)), namespaced and typed
tool.execute.after✅ success only✅ a union on status — failures are visible too

shell.env is the load-bearing one: it is how a session id reaches mcpx without the agent ever seeing it. Its v2 replacement drops exactly the field that makes it work.

The v2 answer is better than a workaround, though. v2 has PUT /api/experimental/session/:id/environment (packages/protocol/src/groups/session.ts:786-795), which sets a session’s shell environment once per session rather than once per command. That is the API v2 added for this, and it turns a per-command hook into a per-session call.

2.3 $ is gone

v1 hands plugins Bun’s shell (packages/plugin/src/index.ts:56-66). In v2 packages/plugin/src/effect/shell.ts is seventeen lines of interface and no executor — verified directly against the pinned source.

import("node:child_process") still works, since plugins are import()ed into the server process in both versions and nothing sandboxes them. But the runtime no longer offers a way to run a command, and that is a clear signal about direction.

mcpx’s plugin uses $ for exactly two things: mcpx --json status to find the daemon, and mcpx log record as the fallback when it cannot. Both must stop being required. That is what §3 is about, and v2 turns it from a performance argument into a portability requirement.

2.4 What a plugin can do to the user

This is the part that surprised me most: the server-side plugin API is almost UI-less in both versions. What a server plugin has is the SDK’s client.tui.*, identical in v1 and v2:

showToast{title?, message, variant: info|success|warning|error, duration?}
appendPrompt, submitPrompt, clearPromptedit the composer
executeCommandrun a command by name
openHelp, openSessions, openThemes, openModelsfixed dialogs

There is no generic dialog and no select over HTTP, in either version.

Real UI lives in a separate TUI plugin, loaded in a different realm, with its own API:

v1 TuiPluginApiv2 TUI Context
ask a questionDialogPrompt, DialogSelect<V>prompt(), select<V>() — promises
toastsui.toast(...)ui.toast.show(...)
slash commandsTuiCommand.slash (deprecated, use keymap layers)KeymapCommand.slash + palette: true
persistent statekv.get/setstorage.store(key, {initial}), disk-backed, live-synced
slots12 host slots10 slots with prepend/append/before/after/replace
panels, tabs, routesroutes onlypanels, tabs, router

So a guided “which daemon?” flow that selects rather than types needs a TUI plugin, in both versions. A toast and a suggestion can come from the server plugin alone.

The other route to a question is a tool with a permission prompt: a plugin tool calls ctx.ask({permission, patterns, always, metadata}), and the user sees an allow/deny. That is a yes/no channel available from the server plugin, without a TUI plugin — and always gives “remember this”, which is most of what a daemon choice needs.

⚠️ v1’s permission.ask hook is dead code: declared (packages/plugin/src/index.ts:261), documented, and never once invoked. Implementing it is a silent no-op. v2’s permission.hook("evaluate") is real.

2.5 Calling a model

v1v2
full agent turn✅ client.session.prompt / promptAsync / command✅ same
one-shot completion❌ no such route✅ ctx.generate.text({prompt, model?}) → {text}
in-session generation❌✅ ctx.session.generate(...)
replace the model objectvia the v2-style aisdk hooks present in the v1 tree✅ aisdk.hook("language")

This answers a question from the sampling design directly: in v2 the plugin can answer an MCP sampling/createMessage cheaply, with ctx.generate.text. In v1 the only way is a full agent turn, which is far too heavy for what sampling is.

2.6 opencode’s own MCP client

Relevant because mcpx is an MCP server to opencode:

v1v2
SDK@modelcontextprotocol/sdk@1.29.0 + a 647-line downstream patch@modelcontextprotocol/client@2.0.0, one hunk
declared client capabilitiesroots only (sampling, elicitation, tasks are commented out with issue links)roots + elicitation (form and url)
sampling/createMessage❌❌
protocol negotiation❌ fixed to the SDK’s latest✅ legacy | auto | 2026-07-28 per server
transportsstdio, Streamable HTTP, SSEStreamable HTTP, hand-written stdio. No SSE
server log forwarding✅❌ regression
prompts/resources list_changed❌✅
OAuthPKCE + DCR, shared callback port+ CIMD, RFC 8707 resource binding, ephemeral callback
execution timeoutone value, 30 sthree tiers; execution 12 h

Consequences for mcpx:

  • Neither version will ever send mcpx a sampling request, because neither declares the capability. mcpx’s sampling passthrough is for other clients.
  • v2 can answer mcpx’s elicitations; v1 cannot, and will get cancel. Until v2, elicitation has to reach the user another way — which is what the broker, mcpx elicit and /v1/elicit are for.
  • v2 negotiating 2026-07-28 per server means mcpx’s modern-era serving path will actually be exercised.
  • v2’s 12-hour execution timeout makes long exec runs viable without tasks.

2.7 Process model

v1v2
shapeone process, two Bun realms (worker = server, main = TUI)two processes, HTTP/RPC between
default networknone — the TUI talks to the server over an in-process fetch; a port opens only with --portHTTP
plugin isolationnone; import() into the realmnone; import() into the process

“Default network: none” is why the plugin must not assume it can reach anything over TCP, and why the unix socket matters. Bun’s fetch(url, {unix}) works in both, since plugins run under Bun in the server realm either way.


3. Design: finding the daemon without a binary

See the tracking issue for the full problem statement. The short version: today the plugin runs mcpx --json status once per session, and if there is no mcpx on PATH, it has nothing. In v2 it will not even have $.

3.1 The ladder

Each rung answers “which daemon?” and stops when it has an answer.

#rungcostwhen it applies
0explicit: MCPX_DAEMON_ENDPOINT, MCPX_SOCKET, or plugin optionsfreeconfigured, or remote
1cached choice for this directory, still aliveone health checkevery session after the first
2read the daemon info filesone readdir + one health check eachno binary needed
3ask a live daemon to resolve the directoryone requesttwo or more candidates
4mcpx --json status in the session directoryone spawn, ~23 msa binary exists and rungs 2–3 were ambiguous
5ask the usera toast, then a choicestill ambiguous
6pick the best candidate, warn, keep workingfreeheadless, or the user did not answer

Rung 6 is the rule the user set: never fail because there are two.

3.2 Rung 2: the info files already exist

The daemon writes daemon-<hash>.json into its state directory with pid, socket, endpoint, configPath, configHash, version and startedAt (daemon.Paths.WriteInfo). Everything discovery needs is already on disk. Reading them needs node:fs, which both versions have, and no binary at all.

Candidates are then filtered to those actually listening — a GET /v1/health over each socket, in parallel, a few milliseconds total. A pid file whose daemon is gone is not a candidate.

This rung alone resolves the single-daemon case, which is almost every case, with no mcpx on PATH.

3.3 Rung 3: any daemon can answer for all of them

A daemon already knows how mcpx resolves configuration for a directory. So one live daemon can answer “which socket serves /path/to/project?” for any directory, including ones it does not serve itself:

GET /v1/resolve?dir=/path/to/project  →  { socket, configPath, configHash, running }

That turns an ambiguous scan into a definite answer without a binary, using a daemon that is already running. It is a small addition to the daemon API and makes rung 4 a rarity.

Without it, the cheap approximation is to walk up from the session directory to the nearest .mcpx.json and compare it against each info file’s configPath. That is right most of the time and wrong exactly when configuration is inherited from a parent — which is the case the fingerprint exists to handle.

3.4 Rung 5: asking, in ascending order of capability

a. Toast + suggestion (server plugin, v1 and v2). On boot and on the first mcpx tool use:

mcpx: 2 daemons match this directory. Using nix (started 3 h ago, 4 servers). /mcpx-daemon to change.

Rate-limited to once per session per reason, so it cannot become noise.

b. A tool with a permission prompt (server plugin, v1 and v2). An mcpx_daemon_select tool whose execution asks ctx.ask(...). The user sees an allow/deny with the daemon’s details and an “always” option. This is the only interactive channel a v1 server plugin has, and “always” is exactly “remember this choice”.

c. A real picker (TUI plugin). v1 DialogSelect, v2 select(). Shows each candidate’s start time, config path, server count and live connections, best first. After choosing, a second question offers: this session only / until it goes away / permanently in config / and optionally stop the others.

d. The guided-skill variant. Instead of a coded walkthrough, a skill tells the agent to call mcpx_daemon_status, reason about which daemon fits, and then call mcpx_daemon_select with that candidate’s index, which is what actually sets the socket. The agent proposes; the tool decides. This is worth doing because it degrades correctly: the agent cannot set the wrong socket by typing a path wrong, because it does not type the path.

⚠️ The gating idea — block every mcpx tool until a daemon is chosen — is a trap. It converts “we guessed” into “nothing works”, which is the failure mode the user ruled out. Better: proceed with the best candidate, and let every affected tool result carry one line saying which daemon answered and how to change it.

3.5 Remembering

scopewherelifetime
this sessionplugin memorythe session
until it goes awayv2 ctx.storage; v1 needs a fileuntil that socket stops answering
permanentdaemon.endpoint in the user’s mcpx configuntil changed

v1 server plugins have no storage API, so “until it goes away” means a small JSON file under the mcpx state directory. v2 has ctx.storage, namespaced per plugin. This is a real v1-workaround-fixed-in-v2.

3.6 Headless

opencode run, CI, a GitHub Action: nobody can answer. There, rung 5 is skipped entirely and rung 6 applies — best candidate, one warning line into the log, carry on. The plugin can tell it is headless by whether a TUI client is attached.

3.7 Also needed for “no binary”

Discovery is only half. The plugin’s other $ use is mcpx log record, and its opt-in tools (mcpx_discover, mcpx_exec, mcpx_observe) each spawn mcpx. Those must move onto the daemon API:

plugin featuretodayneeds
tool timingPOST /v1/log ✅, no fallbacknothing; with no daemon the record is dropped
mcpx_discovermcpx types / mcpx lsGET /v1/types, GET /v1/namespaces ✅ (exist)
mcpx_execmcpx exec <src>POST /v1/exec ✅ (exists)
mcpx_observemcpx log / mcpx statsGET /v1/log, GET /v1/stats ✅ (exist)

exec over /v1 was the one real gap, and it has since landed as POST /v1/exec; the streaming-output question it raised is answered by the frames exec.output selects (docs/exec.md).


4. What v2 changes about all of this

Things that become unnecessary:

  • the mcpx/ subdirectory (only default is read)
  • hand-namespacing tool names (tool.transform namespaces them)
  • a state file for the remembered daemon (ctx.storage)
  • the shell.env per-command hook (session environment API, once per session)

Things that become possible:

  • answering MCP sampling with ctx.generate.text — cheap, no agent turn
  • registering MCP servers per session (mcp.transform), so mcpx could hand opencode a filtered server set rather than a hand-edited config
  • putting a budgeted catalog into session.hook("context") → event.tools instead of appending to the system prompt, with diffs between turns
  • seeing tool failures in execute.after

Things that break:

  • the plugin itself: v1 plugins do not load, so v2 needs a rewrite, not a port
  • $
  • sessionID in the shell hook
  • MCP SSE transport, and MCP server log forwarding (both regressions in v2)

Things that are the same:

  • client.tui.showToast and the rest of the TUI SDK surface
  • no isolation; plugins run in the host’s realm
  • Bun’s fetch(url, {unix}), so the socket fast path survives
  • one MCP connection per server per location — the chrome-sharing problem OPENCODE-V2.md describes is unchanged

5. What is opencode, and what is general

For porting to codex, claude, pi, manus, this is the part that matters.

concerngeneralopencode-specific
find the daemonthe whole ladder in §3. Info files, health checks, a resolve endpoint, a cached choicewhere config lives; how “the current directory” is known
inject a session idthat mcpx wants one, to attribute calls and route elicitationsshell.env (v1) / session environment API (v2). Every harness has a different hook, or none
tell the model mcpx existsa short, budgeted description; never the tool schemasexperimental.chat.system.transform (v1) / session.hook("context") (v2)
offer toolsdiscover, exec, observe as a minimal settool() + the map (v1) / tool.transform (v2)
record timingsPOST /v1/log over the socketwhich hook fires after a tool call, and what it carries
ask the userthat a question must have a default and a deadlinetoast vs permission prompt vs TUI dialog
remember a choicesession / until-gone / permanentctx.storage vs a file
answer samplinga harness with a model should answer; one without should declinectx.generate.text (v2 only)

The shape that ports: a small core that speaks the daemon API over a unix socket, plus a thin adapter per harness. The core is plugin/opencode/mcpx/daemon.ts today, and it is already almost harness-free — it takes $ only for the rung-4 spawn, which §3 removes. Lifting it into a harness-free core directory beside opencode once a second harness exists would make the split explicit.

What each harness needs to provide its adapter:

  1. a directory (to resolve which daemon)
  2. a session identifier (to attribute and route)
  3. somewhere to put environment variables, or a way to wrap tool calls
  4. a hook after a tool call, if timings are wanted
  5. a way to show a line of text, if ambiguity is to be reported
  6. optionally, a way to ask a question and a way to run a model

Only (1) and (2) are required. Everything else degrades.


6. Per-part reference

shell.env → session id injection

  • What. Sets MCPX_SESSION_ID, MCPX_CALL_ID and friends in the environment of every shell command opencode runs; the full list, with who reads each, is generated in environment.md.
  • Why. mcpx needs a session id to attribute calls, lease stateful servers and route elicitations. Passing it through the model’s context would spend tokens on something the model neither chooses nor should see.
  • How. v1: the shell.env hook, per command, carrying sessionID and callID. v2: one PUT /api/experimental/session/:id/environment per session.
  • When it degrades. No hook, no ids: mcpx attributes calls to “unknown” and leases fall back to whole-daemon scope.
  • General or opencode? The need is general. The mechanism is entirely opencode’s.

The system-prompt line

  • What. A short paragraph telling the model mcpx exists and when to reach for it.
  • Why. The whole point of mcpx is that tool schemas stay out of context. The model still has to know the door is there.
  • How. v1 experimental.chat.system.transform; v2 session.hook("context").
  • When it degrades. Without it, the opt-in tools still work when named.

The opt-in tools

  • What. mcpx_discover, mcpx_exec, mcpx_observe, off by default.
  • Why. An agent that can write a script against the catalog beats one that can only call tools one at a time. Off by default because each adds a tool to every request’s tool list, which is exactly the cost mcpx exists to avoid.
  • How. v1 the tool map (watch for name collisions — it is not namespaced); v2 tool.transform with a namespace.

The timing hook

  • What. One log record per opencode tool call, into mcpx’s log.
  • Why. One mcpx stats then covers the harness and mcpx on one timeline.
  • How. POST /v1/log over the socket, 0.09 ms. No spawn fallback.
  • When it degrades. No daemon: the record is dropped (plugin/opencode/mcpx-session.ts:783). Dropping is correct — a timing is not worth 23 ms on every tool call, and certainly not worth failing one.

The socket client

  • What. plugin/opencode/mcpx/daemon.ts: find a daemon, then speak /v1 to it.
  • Why. 0.17 ms against 23 ms for a spawn, on a path that runs for every tool call. And in v2 there is no spawn available at all.
  • How. Bun’s fetch(url, {unix}). Node’s fetch cannot do this without undici, so a Node harness uses the TCP endpoint instead.
  • General or opencode? Almost entirely general. This is the part to extract when a second harness appears.

Overview

mcpx puts every MCP server you have behind one command, so an agent calls tools from the shell — and their schemas stay out of its context until it asks for them.


Why install it

1. Code mode: tools become shareable, reusable scripts

Instead of one-off tool calls inside a chat, the agent writes a TypeScript script that calls tools as ordinary async functions. The script is a file: it can be saved, reviewed, shared and re-run.

mcpx run shot-flow <url> <outDir>

That is .mcpx/scripts/shot-flow.ts — screenshot a page, click a link, wait for the load, screenshot again (the full walk is in the story). Written once by an agent that had never heard of mcpx; reusable by anyone, forever, as mcpx run shot-flow.

More: scripts and the level-2 map’s script page (level-2 page map).

2. Reclaim context: smarter, cheaper

Tool schemas are the tax. With seven namespaces and 43 tools installed, discovery costs almost nothing:

$ mcpx ls
Seven namespaces, 43 tools, 171 tokens

The agent loads signatures only for the one namespace it is about to use (mcpx types <namespace>), so the whole screenshot task above ran in about 4,600 tokens total — schemas for 27 browser tools included only when needed, never by default.

More: the story for the token evidence.

3. Tame MCP: one config, every surface, everything corralled

  • One config. mcpx reads the same mcpServers object every other MCP host reads. There is no second configuration to keep in sync.

  • Share it with your tailnet. Run the daemon on one machine and let the others talk to it:

    mcpx daemon --address <tailnet-ip> --port 8899
    MCPX_DAEMON_ENDPOINT=http://<host>:8899 mcpx ls
    

    Plainly: the daemon’s API is unauthenticated — whatever can route to the port can run tools as you. The network is the access control; that is why the example is a tailnet address, not the open internet.

  • Corral scripts, APIs and binaries. Named scripts live in .mcpx/scripts/; an OpenAPI service becomes tools with mcpx api tools --spec <url> (served over MCP today); a plain command-line program becomes tools through an adapter (mcpx adapter check says whether each one is installed), and adapters are served by the daemon like any other server.

More: configuration.

What it looks like

flowchart TB
    subgraph clients["You and your agents"]
        CLI["CLI — mcpx ls | search | types | run | exec"]
        PLUGIN["opencode plugin — no mcpx binary needed"]
        MCP["MCP host — /mcp on the daemon, or mcpx serve"]
        HTTP["HTTP client — /v1/* (OpenAPI at /v1/openapi.json)"]
    end
    subgraph daemon["mcpx daemon — one per config, autostarts, idle-exits"]
        POOL["pooled MCP server processes (stdio) and remote servers (HTTP)"]
        EXTRA["OpenAPI specs and CLI adapters, as tools"]
        RUNNER["script runner: deno | bun | node subprocess"]
    end
    CLI --> daemon
    PLUGIN --> daemon
    MCP --> daemon
    HTTP --> daemon
    daemon --> POOL
    daemon --> EXTRA
    daemon --> RUNNER
    RUNNER -. "calls back into the daemon" .-> daemon

What is running on your machine, in one breath: clients (the CLI, the plugin, an MCP host, plain HTTP) talk over a unix socket or a URL to one daemon per config. The daemon owns the pooled MCP server processes, serves OpenAPI specs and CLI adapters as tools, and runs your scripts on a real runtime (deno, bun or node) whose tool calls come back into the daemon. Nothing else is installed and nothing runs until a command needs it: the daemon autostarts on demand and an auto-started one exits after 4 hours idle.

Next

Parity

Everything the CLI can do can be done from a plugin, can be done over /v1, can be done from MCP.

That is a claim, and this file is generated from the four declarations it is about, so it cannot describe a surface that does not exist or leave one out. Each direction is also held by a test:

DirectionHeld by
every /v1 route has an operation, and every operation a routeTestEveryDaemonRouteIsDeclared, TestEveryDeclaredOpHasARoute
every operation that is not a stream has an MCP toolTestEveryNonStreamingOpIsReachableAsAnMCPTool
every operation has a CLI command, and the generated ones reach their routeTestEveryOperationIsReachableFromTheCLI, TestEveryGeneratedCommandReachesItsRoute
every command the binary runs is declared, and every declared one runsTestEveryDispatchedCommandIsDeclared
every command reaches an operation, or says why it cannotTestEveryCommandReachesAnOperationOrSaysWhy
every operation has a plugin method, and they work against a real daemonTestPluginOpsAreGenerated, TestThePluginSuiteRunsAgainstARealDaemon
this fileTestParityDocumentIsCurrent

A command marked generated is built from the operation’s declaration – flags from its parameters, positionals for its path – so it exists the moment the operation does. The plugin reaches every operation through DaemonClient.ops (plugin/opencode/mcpx/ops.gen.ts, generated the same way); the named methods beside it are conveniences with typed answers.

Privileged operations are offered rather than hidden. A caller that can reach the socket can already stop the daemon, so withholding the tool buys no safety and costs an agent the ability to restart a server that has wedged. What they carry instead is a description that says so and MCP annotations (readOnlyHint, destructiveHint) for a client that wants to scope on them.

/v1 operations (83)

OperationRouteCLIMCP toolPluginNotes
adapters_addPOST /v1/adaptersmcpx adapter addmcpx_adapters_addops.adaptersAdd()Privileged.
adapters_removeDELETE /v1/adapters/{name}mcpx adapter removemcpx_adapters_removeops.adaptersRemove()Privileged. Destructive.
artifacts_listGET /v1/artifactsmcpx artifact list generatedmcpx_artifacts_listops.artifactsList()
artifact_putPOST /v1/artifactsmcpx artifact put generatedmcpx_artifact_putops.artifactPut()Privileged.
artifact_deleteDELETE /v1/artifacts/{id}mcpx artifact delete generatedmcpx_artifact_deleteops.artifactDelete()Privileged. Destructive.
artifact_getGET /v1/artifacts/{id}mcpx artifact get generatedmcpx_artifact_getops.artifactGet()
ask_beginPOST /v1/askmcpx ask begin generatedmcpx_ask_beginops.askBegin()
ask_pollGET /v1/ask/{id}mcpx ask poll generatedmcpx_ask_pollops.askPoll()
ask_abandonPOST /v1/ask/{id}/abandonmcpx ask abandon generatedmcpx_ask_abandonops.askAbandon()Privileged. Destructive.
ask_answersPOST /v1/ask/{id}/answersmcpx ask answers generatedmcpx_ask_answersops.askAnswers()Privileged.
batchPOST /v1/batchmcpx batchmcpx_batchops.batch()
callPOST /v1/callmcpx callmcpx_callops.call(), call(), callAsTask()
call_pathPOST /v1/call/{server}/{tool}mcpx callmcpx_callops.callPath()
catalogGET /v1/catalogmcpx catalogmcpx_catalogops.catalog()
catalog_historyGET /v1/catalog/historymcpx history generatedmcpx_catalog_historyops.catalogHistory()
chat_completionsPOST /v1/chat/completionsmcpx proxymcpx_chat_completionsops.chatCompletions()
client_moduleGET /v1/client.tsmcpx clientmcpx_client_moduleops.clientModule()
completePOST /v1/completemcpx complete generatedmcpx_completeops.complete(), complete()
daemon_restartPOST /v1/daemon/restartmcpx restartmcpx_daemon_restartops.daemonRestart()Privileged. Destructive.
dashboardGET /v1/dashboardmcpx dashboardmcpx_dashboardops.dashboard()
diagnosePOST /v1/diagnosemcpx diagnosemcpx_diagnoseops.diagnose()
elicit_listGET /v1/elicitmcpx elicit listmcpx_elicit_listops.elicitList(), pendingElicitations()
elicit_getGET /v1/elicit/{id}mcpx elicit showmcpx_elicit_getops.elicitGet()
elicit_answerPOST /v1/elicit/{id}/{action}mcpx elicit answermcpx_elicit_answerops.elicitAnswer(), answer()Privileged.
embeddingsPOST /v1/embeddingsmcpx embeddingsmcpx_embeddingsops.embeddings()
embeddings_exportGET /v1/embeddings/exportmcpx embeddings exportmcpx_embeddings_exportops.embeddingsExport()
embeddings_importPOST /v1/embeddings/importmcpx embeddings importmcpx_embeddings_importops.embeddingsImport()
eventsGET /v1/eventsmcpx events generatednone: a stream; MCP carries it as subscriptions/listenops.events()Never ends; read it until you are done.
execPOST /v1/execmcpx execmcpx_execops.exec(), exec()Privileged.
feedbackPOST /v1/feedbackmcpx feedbackmcpx_feedbackops.feedback()
globalsGET /v1/globals.d.tsmcpx globals generatedmcpx_globalsops.globals()
healthGET /v1/healthmcpx health generatedmcpx_healthops.health(), health()
intentPOST /v1/intentmcpx promptmcpx_intentops.intent()
interactions_listGET /v1/interactionsmcpx interactionsmcpx_interactions_listops.interactionsList()
interactions_getGET /v1/interactions/{id}mcpx interactions showmcpx_interactions_getops.interactionsGet()
log_queryGET /v1/logmcpx logmcpx_log_queryops.logQuery(), logQuery()
log_recordPOST /v1/logmcpx log recordmcpx_log_recordops.logRecord(), record()Privileged.
metrics_tokensGET /v1/metrics/tokensmcpx metrics tokens generatedmcpx_metrics_tokensops.metricsTokens()
metrics_toolsGET /v1/metrics/toolsmcpx metrics tools generatedmcpx_metrics_toolsops.metricsTools()
namespacesGET /v1/namespacesmcpx lsmcpx_namespacesops.namespaces(), namespaces()
openapiGET /v1/openapi.jsonmcpx openapimcpx_openapiops.openapi()
preflightPOST /v1/preflightmcpx preflight generatedmcpx_preflightops.preflight()
prompt_getPOST /v1/promptmcpx promptsmcpx_prompt_getops.promptGet()
promptsGET /v1/promptsmcpx promptsmcpx_promptsops.prompts()
protocolGET /v1/protocolmcpx protocol generatedmcpx_protocolops.protocol()
recipes_listGET /v1/recipesmcpx recipes listmcpx_recipes_listops.recipesList()
recipe_getGET /v1/recipes/{name}mcpx recipes showmcpx_recipe_getops.recipeGet()
recipe_savePOST /v1/recipes/{name}mcpx recipes savemcpx_recipe_saveops.recipeSave()
recipe_runPOST /v1/recipes/{name}/runmcpx recipes runmcpx_recipe_runops.recipeRun()
refreshPOST /v1/refreshmcpx refreshmcpx_refreshops.refresh()Privileged.
registry_searchGET /v1/registry/searchmcpx registry searchmcpx_registry_searchops.registrySearch(), registrySearch()
reloadPOST /v1/reloadmcpx reloadmcpx_reloadops.reload()Privileged.
resolveGET /v1/resolvemcpx resolve generatedmcpx_resolveops.resolve(), resolve()
resource_readPOST /v1/resourcemcpx resourcesmcpx_resource_readops.resourceRead()
resource_templatesGET /v1/resource-templatesmcpx resources --templatesmcpx_resource_templatesops.resourceTemplates()
resourcesGET /v1/resourcesmcpx resourcesmcpx_resourcesops.resources()
restartPOST /v1/restartmcpx restartmcpx_restartops.restart()Privileged. Destructive.
searchGET /v1/searchmcpx searchmcpx_searchops.search()
secret_requests_listGET /v1/secret-requestsmcpx secrets requestsmcpx_secret_requests_listops.secretRequestsList()
secret_request_declineDELETE /v1/secret-requests/{id}mcpx secrets declinemcpx_secret_request_declineops.secretRequestDecline()Privileged.
secret_request_getGET /v1/secret-requests/{id}mcpx secrets requestmcpx_secret_request_getops.secretRequestGet()
secrets_listGET /v1/secretsmcpx secrets listmcpx_secrets_listops.secretsList()
secrets_unsetDELETE /v1/secrets/{name}mcpx secrets unsetmcpx_secrets_unsetops.secretsUnset()Privileged. Destructive.
secrets_setPUT /v1/secrets/{name}mcpx secrets setnone: a value sent as a tool argument would sit in the model’s context and transcript; the CLI reads it from standard input and OpenChamber from a password fieldops.secretsSet()Privileged.
servers_listGET /v1/serversmcpx servers listmcpx_servers_listops.serversList()
servers_addPOST /v1/serversmcpx servers addmcpx_servers_addops.serversAdd()Privileged.
servers_removeDELETE /v1/servers/{name}mcpx servers removemcpx_servers_removeops.serversRemove()Privileged. Destructive.
servers_getGET /v1/servers/{name}mcpx servers showmcpx_servers_getops.serversGet()
session_releasePOST /v1/session/releasemcpx session release generatedmcpx_session_releaseops.sessionRelease()Privileged.
settings_listGET /v1/settingsmcpx settings listmcpx_settings_listops.settingsList()
settings_unsetDELETE /v1/settings/{path}mcpx settings unsetmcpx_settings_unsetops.settingsUnset()Privileged.
settings_getGET /v1/settings/{path}mcpx settings getmcpx_settings_getops.settingsGet()
settings_setPUT /v1/settings/{path}mcpx settings setmcpx_settings_setops.settingsSet(), putSetting()Privileged.
shutdownPOST /v1/shutdownmcpx stopmcpx_shutdownops.shutdown(), shutdown()Privileged. Destructive.
stats_queryGET /v1/statsmcpx statsmcpx_stats_queryops.statsQuery(), stats()
statusGET /v1/statusmcpx statusmcpx_statusops.status(), status()
tasks_listGET /v1/tasksmcpx task list generatedmcpx_tasks_listops.tasksList(), tasks()
task_getGET /v1/tasks/{id}mcpx task get generatedmcpx_task_getops.taskGet(), task()
task_cancelPOST /v1/tasks/{id}/cancelmcpx task cancel generatedmcpx_task_cancelops.taskCancel(), cancelTask()
task_resultGET /v1/tasks/{id}/resultmcpx task result generatedmcpx_task_resultops.taskResult(), taskResult()
toolsGET /v1/toolsmcpx tools generatedmcpx_toolsops.tools()
tool_invokePOST /v1/tools/{tool}mcpx tool invoke generatedmcpx_tool_invokeops.toolInvoke()
typesGET /v1/typesmcpx typesmcpx_typesops.types(), types()

CLI commands (69)

Every command the binary dispatches, with the operations it reaches. A command that reaches none says why: those are decisions, not gaps.

CommandOperationsNotes
adapteradapters_add, adapters_remove
api–OpenAPI declarations are files the CLI reads; the tools they produce are served over MCP, and are not yet a tool source the daemon owns (#83).
artifact / artifactsartifacts_list, artifact_put, artifact_delete, artifact_getGenerated from the operation table.
askask_begin, ask_poll, ask_abandon, ask_answersGenerated from the operation table.
batchbatch
cache–Reads and prunes the cache directory on this machine; no daemon is involved.
callcall, call_path
catalogcatalog
clientclient_module
completecompleteGenerated from the operation table.
completion–Text about the binary, produced by the binary.
config–Reads the configuration files on the caller’s search path; settings is the running daemon’s view.
daemon–Starts a daemon, which a daemon’s own API cannot do.
daemons–Reads every daemon’s info file on this machine; one daemon cannot see the others. Daemons with their own state directory mirror their record into the default one, and records whose process is gone are pruned.
dashboarddashboard
diagnosediagnose
doctor–Checks this machine: binaries on PATH, permissions, configuration files.
elicitelicit_list, elicit_get, elicit_answer
embeddingsembeddings, embeddings_export, embeddings_import
eventseventsGenerated from the operation table.
execexec
explore–An interactive interface over the reads above; it consumes the API rather than extending it.
feedbackfeedback
globalsglobalsGenerated from the operation table.
healthhealthGenerated from the operation table.
help–Text about the binary, produced by the binary.
historycatalog_historyGenerated from the operation table.
hook–Reads the resolved hooks setting from the configuration chain on this machine.
init–Writes a file into the current directory.
interactionsinteractions_list, interactions_get
log / logslog_query, log_record
ls / list / namespacesnamespaces
man–Text about the binary, produced by the binary.
metricsmetrics_tokens, metrics_toolsGenerated from the operation table.
openapiopenapi
plugin–Plugin discovery is local: the CLI and daemon read paths.plugins and PATH on this machine, and the daemon serves each plugin as a server of its own.
preflightpreflightGenerated from the operation table.
promptintent
promptsprompt_get, prompts
protocolprotocolGenerated from the operation table.
proxychat_completions
recipesrecipes_list, recipe_get, recipe_save, recipe_run
refreshrefresh
registryregistry_search
reloadreload
resolveresolveGenerated from the operation table.
resourcesresource_read, resource_templates, resources
restartdaemon_restart, restart
run–Resolves a script name against the caller’s search path, which only the caller has; the source itself runs anywhere through exec.
schema–Reshapes what tools and types return into JSON Schema, OpenAPI or MCP form; the data is those operations’, the conversion is local.
scripts–Lists the caller’s script search path.
searchsearch
secretssecret_requests_list, secret_request_decline, secret_request_get, secrets_list, secrets_unset, secrets_set
serve–It is the MCP surface: a stdio shim over the daemon, which serves the same tools at /mcp itself.
serversservers_list, servers_add, servers_remove, servers_get
sessionsession_releaseGenerated from the operation table.
settingssettings_list, settings_unset, settings_get, settings_set
skills–Manages embedded agent skills.
statsstats_query
statusstatus
stdio-proxy–It is a byte pipe: it dials the daemon’s unix socket on this host and copies stdin/stdout to it, which no /v1 operation can express.
stopshutdown
supervise–Runs in its own process as the re-exec target of the supervised transport; it serves no API.
task / taskstasks_list, task_get, task_cancel, task_resultGenerated from the operation table.
tooltool_invokeGenerated from the operation table.
toolstoolsGenerated from the operation table.
tui–An interactive interface over the reads above; it consumes the API rather than extending it.
typestypes
upgrade–Replaces the running daemon with another binary, a decision for whoever runs the CLI rather than a tool a model can call.

Hand-written MCP tools (12)

The tools mcpx offered before the operation table existed. Where one already covers an operation, no second tool is generated for it.

ToolCoversSummary
batch_call–Alias for mcpx_batch.
mcpx_batchbatchExecute one or more tools in a single turn.
mcpx_callcall, call_pathCall one tool on one server.
mcpx_catalogcatalogEvery namespace with as many tool signatures as fit a token budget.
mcpx_execexecRun TypeScript against every server at once.
mcpx_log–Query mcpx’s durable log: what ran, what it cost, what failed.
mcpx_namespacesnamespacesList every MCP server mcpx knows about, with tool counts.
mcpx_registry–Search a public registry of MCP servers that are not yet configured here.
mcpx_searchsearchFind tools by name and description across every server.
mcpx_stats–Aggregate the log: calls, servers, errors, sessions, slowest, volume.
mcpx_statusstatusThe daemon, its pools and live instances.
mcpx_typestypesFull TypeScript signatures for named namespaces, including each server’s own guidance.

Boundaries (5)

Every restriction mcpx declares, and whether anything enforces it (decisions/0003). An enforced one names the tests that try to get round it; an advisory one names the issue that will enforce it. TestEveryEnforcedBoundaryNamesItsRefusal holds both.

RestrictionDeclaresStateEvidence
transport.allowedOriginsa web page on an origin that is not loopback, the daemon’s own address or listed in transport.allowedOrigins cannot use /v1 or /mcpenforcedTestAWebPageCannotRunCodeThroughTheDaemon, TestV1AndMCPAgreeOnOrigins
script.permissionsa script runs with no more than the permission profile named; a runtime with no permission model refuses a narrowed profileenforcedTestScriptPermissionsIsReachableFromEverySurface, TestAStrictScriptCannotReadFilesOnARuntimeWithoutPermissions, TestARestrictedProfileIsRefusedByARuntimeThatCannotEnforceIt
elicit.confirmDestructivewith it on, a call to a tool that may be destructive (annotated destructiveHint, or unannotated) does not run unless somebody confirms it; a remote server’s annotation never clears the call (#2191)enforcedTestADestructiveCallIsRefusedWhenNobodyConfirms, TestConfirmDestructiveHoldsOnAColdDaemon
autonomy.maxmcpx does no more on its own than the daemon’s autonomy.max allows: a prompt, a recipe run by name or a configured hook asked for above it is lowered to it and the answer says so, from /v1, the CLI, MCP and the plugin alike. It does not govern code a caller sends to /v1/exec itselfenforcedTestV1CannotRaiseAutonomyAboveTheCeiling, TestTheCeilingCannotBeRaisedAtRuntime, TestTheCLICannotRaiseAutonomyAboveTheCeiling, TestMCPCannotRaiseAutonomyAboveTheCeiling, TestThePluginCannotRaiseAutonomyAboveTheCeiling, TestHooksFromConfigDoNotRunAboveTheCeiling, TestRepairCannotRaiseAutonomyAboveTheCeiling, TestACallerCannotRaiseAClampedSetting
privileged operations (Admin)operations marked privileged change the daemon rather than read it; nothing checks who calls them, and every tool description and the OpenAPI document say soadvisory#253

Finding the daemon without the binary

What the opencode plugin does when there is no mcpx on PATH, why each rung of the ladder exists, and what was found building it.

id:           finding-the-daemon-without-the-binary
since:        pre-release
created:      2026-09-29T21:00:00-05:00
last-updated: 2026-09-29T21:00:00-05:00
status:       implemented

Companion to docs/opencode-plugin.md, which records what opencode v1 and v2 offer a plugin. This one records the decisions that followed, and the things that turned out not to be true.


1. The requirement, restated

The binary must not be a precondition. Two reasons, one of them forced:

  • a daemon may be running under launchd, in a container, or on another host, with no mcpx on the plugin’s PATH;
  • opencode v2 removes Bun’s $ from the plugin API. node:child_process still works, but the runtime no longer offers a way to run a command, and building on one it does not offer is building on sand.

And the harder half: never fail because there are two. A plugin that refuses to work until the user disambiguates has converted “we guessed” into “nothing works”, which is worse than the guess.


2. GET /v1/resolve

The one new daemon endpoint. It answers “which daemon serves this directory?” for any directory, from any live daemon, because the answer is a function of mcpx’s configuration search path and not of what the asked daemon happens to have loaded.

GET /v1/resolve?dir=/abs/path
  → { socket, endpoint, configPath, configHash, running, sources }

Three things were needed to make it honest.

It must not chdir. config.SearchPath() walked up from os.Getwd(), which a server cannot change without racing every request in flight. So SearchPathFrom(dir) and LoadFrom(explicit, dir) were added and the existing functions now delegate to them. That is the only edit outside this feature’s own files.

It must resolve symlinks first. FingerprintConfig already resolves the files it hashes, for exactly this reason: /tmp is a symlink to /private/tmp on macOS, so a caller whose $PWD holds the unresolved spelling computed a different key for the same project and was told nothing was running. The directory has to get the same treatment before the walk, or the bug comes back one level up.

It must prefer the info file’s socket. The daemon key names the socket only when the state path fits in sun_path; beyond 100 bytes the socket moves to a private runtime directory and only the info file knows where. A resolver that trusted the key would hand out a path with nothing behind it.

A name collision worth recording

daemon-<key>.json contains a field called configHash, and it is not the key in the file name. The field is HashConfig(raw) — a digest of the server definitions, used to invalidate the schema cache. The file name is FingerprintConfig(sources) — the daemon key, over every path and every byte that contributed. Two different numbers for the same daemon, under one name.

/v1/resolve returns the key, because that is what identifies a daemon. Renaming the field in the info file would be the real fix; it is out of scope here and is reported rather than done.


3. The ladder

#rungcostapplies when
0explicit endpoint or socketfreeconfigured, or remote
1the remembered choice, still answeringone health checkevery session after the first
2the daemon-*.json info filesreaddir + parallel healththe usual case
3/v1/resolve against any live daemonone requesttwo or more candidates
4mcpx --json statusone spawn, ~23 msa binary exists and rung 3 was not available
5aska toast, a prompt, or the pickerstill ambiguous, and someone is there
6best candidate, warn, carry onfreeheadless, or unanswered

Rung 5 is deliberately not in the core. The core returns the candidates and a warning; whether anyone can be asked, and how, is the adapter’s business, and it differs per harness. That keeps mcpx/daemon.ts free of opencode.

Rung 2 looks in three directories, not one

The info file is always in the state directory. The socket is not: too long a state path sends it to $XDG_RUNTIME_DIR/mcpx, and failing that to $TMPDIR/mcpx-<uid>. Sockets are therefore scanned in all three, and any that has no info file is still health-checked — a daemon with a deleted info file is still a daemon.

A socket that is not owned by this user is skipped without dialing. The socket’s permissions are the access control for the whole API; connecting to another user’s would send this session’s calls to somebody else’s servers.

Rung 3 stops at the first definite answer

If a daemon answers and names a socket that is live and among the candidates, that is the answer. If it answers and says nothing is serving this directory, the loop stops rather than asking the next daemon: they all compute the same function, so a second opinion is the same opinion.

Rung 0 does not fall through

A named endpoint that is not answering returns that endpoint with a warning, never a local daemon. Answering from the wrong machine — wrong servers, wrong credentials, possibly wrong data — is worse than answering not at all. This matches what the CLI already does (will not start a local one).


4. Asking, in a harness with almost no interface

A v1 server plugin has client.tui.showToast and a few fixed dialogs. There is no generic dialog and no select over HTTP, in either version. So:

  • A toast, on boot and on first use, once per session per reason.
  • One line on every mcpx tool result, naming the daemon that answered. Cheaper than a toast and impossible to miss in a transcript.
  • mcpx_daemon_select, whose execution raises ctx.ask(...). That is the only interactive channel a v1 server plugin has, and it is a good one: the user sees the daemon’s config path, start time, version and session count in the permission metadata, and always is exactly “remember this”.
  • mcpx-tui.tsx, optional, for a real list you arrow through.

The tool takes an index, not a path. The agent reads mcpx_daemon_status, reasons about it, explains its recommendation, and names a number; the tool turns that into a socket. The agent cannot mistype a path it never types. This is why the guided skill beats a coded walkthrough: it degrades correctly when the agent is confused.

⚠️ v1’s permission.ask hook is dead code — declared, documented, never invoked. Nothing here is built on it.

Detecting headless

showToast publishes an event and returns true whether or not anyone is listening, so it cannot be asked. The process shape can: opencode tui runs the server in a Worker and the interface on the main thread, while opencode run, opencode serve and every CI invocation run the server on the main thread with nothing attached. So isMainThread from node:worker_threads is, in v1, exactly “no TUI attached”. MCPX_PLUGIN_HEADLESS overrides it.

Being wrong in the safe direction costs a toast nobody sees. Being wrong the other way would hang a headless run on a question — which is why no automatic path ever raises a permission prompt. Only mcpx_daemon_select, which an agent calls deliberately, asks anything.


5. Remembering

scopewherewhy there
sessionplugin memoryit should not outlive the session
until-gone, indefinite$MCPX_STATE_DIR/opencode-daemons.jsonv1 server plugins have no storage API
permanentPUT /v1/settings/daemon.endpointit is a user preference, and mcpx already owns those

The file is keyed by directory, written through a rename, and read at rung 1 before the scan. ctx.storage in v2 replaces it exactly.

The TUI picker writes the same file — it is the only channel between the two realms — so the server plugin re-reads it when its copy has gone stale. Five seconds: never would make the picker take effect only in the next session, and always would put a file read in front of every tool call.


6. What else had to stop spawning

Discovery was half the problem. The rest:

featurebeforenow
tool timingPOST /v1/log, spawn fallbackPOST /v1/log, and drop the record with no daemon
mcpx_discovermcpx ls / mcpx typesGET /v1/namespaces, GET /v1/types
mcpx_observemcpx log / mcpx statsGET /v1/log, GET /v1/stats
mcpx_execmcpx execPOST /v1/exec, falling back on 404

Dropping the timing record is the right failure. A timing is not worth 23 ms on every tool call, and certainly not worth failing one.

The backend setting (auto, v1, cli) makes the fallback explicit. v1 is the honest setting for a machine with no binary: a missing daemon then reports a missing daemon, rather than a missing command.


7. What is general and what is opencode

mcpx/daemon.ts imports nothing from opencode. It uses fetch, node:fs, node:os, node:path and node:child_process, and nothing else. The ladder, the info-file format, the health filtering, the ranking and the remember file are all about mcpx; mcpx-session.ts is the part that is about opencode.

What a second harness has to provide: a directory, a session id, somewhere to put environment variables, and — optionally — a hook after a tool call, a way to show a line of text, and a way to ask a yes/no. Only the first two are required; everything else degrades.


8. Constants

The Go side puts every constant in internal/defaults/defaults.json; this feature adds resolve.dialTimeout.

The plugin cannot read that file — it ships as loose files copied into an opencode config directory, with no mcpx installation implied. So it has the same idea in the only form available: one TUNING table at the top of mcpx/daemon.ts, with every timeout, limit and file name in it, and each value overridable through the options a harness passes in.

Plugins

“Plugin” means two different things in this project, and every page in this set says which one it means:

meaningdirectionexists today
harness plugin: code that plugs mcpx into an agent harnessa consumer of mcpxyes: the opencode plugin, under plugin/opencode/
mcpx plugin: an extension of mcpx itselfextends mcpxno: there is no plugin concept in the daemon at all

Nearly everything below is about the first meaning. The second gets a short status section at the end of the first section, and stays a stub until the extensibility work that designs it lands.

Plugins

Harness plugins: mcpx inside your agent

A harness plugin is optional. Any harness with a shell can use mcpx through AGENTS.md alone: the agent runs mcpx commands, and mcpx needs nothing from the harness to answer them. What the harness cannot give that way is identity — and identity is what the plugin adds:

  • the session id, so session-scoped servers persist across commands and two agents running at once lease separate server processes instead of corrupting one shared browser;
  • the socket fast path: about 0.17 ms per call over the daemon’s unix socket, against about 23 ms for spawning the binary — which is why the plugin also means no mcpx on PATH is required for any of it (the one exception is rung 4 of daemon discovery, below, a last fallback that runs the binary only when one exists);
  • optional extras: tools offered to the model, a system-prompt line, tool timing records, and a picker for choosing between daemons.

Hosts that only speak MCP need no plugin at all: point them at mcpx serve --transport stdio or the daemon’s /mcp endpoint and mcpx is just an MCP server to them. Session scoping still works there, but it is mcpx’s own transport session — a stable per-process key (mcp-<pid> for a stdio server) — not the harness’s session. What those hosts do not get is harness-session identity: nothing ties the MCP connection to the agent session that spawned it, so two agents sharing one MCP server process share its session scope too. That identity is the trade the plugin exists to provide.

mcpx plugins: extending mcpx

Not built. There is no plugin concept in the daemon: no loading, no hooks, no extension API. The design space — Go, binary, script or remote plugins, hooks, variables — is tracked by the extensibility issues on the repository (#80, #82, #83 in the original numbering; see the issue for this page, #1855). Until that work lands, the ways to extend what mcpx can reach are the built-in ones: a command-line program becomes tools through an adapter, an OpenAPI document becomes tools through mcpx api, and a saved script or recipe runs by name. This section stays a stub so the word “plugin” is never left ambiguous: if a page does not say “mcpx plugin”, it means the harness kind.

Plugin compatibility

Every cell cites a source or says “unknown”. Nothing here is guessed, and no support is claimed before an adapter exists.

harnesspluginhow mcpx reaches it todaysession idsource
opencode v1 (1.18.31)yes, plugin/opencode/plugin, or shell + AGENTS.mdinjected by the shell.env hookplugin/opencode/README.md; docs/opencode-plugin.md §2, §6
opencode v2 (2.0.3)no: v1 plugins do not loadshell + AGENTS.md, or mcpx as an MCP server in its mcp confignot injected; needs the v2 session environment APIdocs/opencode-plugin.md §4; see opencode v2 plugin below
codexnoshell + AGENTS.md; MCPunknownadapter design: docs/opencode-plugin.md §5
claude codenoshell + AGENTS.md; MCPunknownadapter design: docs/opencode-plugin.md §5
pinounknownunknownadapter design: docs/opencode-plugin.md §5
manusnounknownunknownadapter design: docs/opencode-plugin.md §5
any MCP hostnot needed/mcp or stdiono harness session; mcpx derives its own transport session (mcp-<pid> for stdio)internal/cli/serve.go

One runtime note: the portable core dials the socket with Bun’s fetch(url, {unix}). A Node-based harness cannot use that option, so it uses MCPX_DAEMON_ENDPOINT and the daemon’s TCP listener instead. Everything else in the core is runtime-neutral.

Plugin interface and config

There are two interfaces — plugin to harness, and plugin to daemon — and one configuration surface.

Plugin to harness

The opencode plugin uses four hooks (docs/opencode-plugin.md §2.2):

hookwhat it carries for mcpxwithout it
shell.envMCPX_SESSION_ID, MCPX_CALL_ID and the rest of the injected environment, per shell commandno ids: calls are attributed to “unknown” and session-scoped leases fall back to per-call isolation — each invocation gets its own lease keyed by its call id, so state does not persist across commands and anything stateful must happen inside one invocation
experimental.chat.system.transformthe opt-in system-prompt line saying mcpx existsthe opt-in tools still work when named; the model may never reach for mcpx
the tool mapthe opt-in tools mcpx_discover, mcpx_exec, mcpx_observemcpx is shell-only, which is the default anyway
tool.execute.afterone timing record per tool call, into mcpx’s logno harness timings; mcpx stats covers mcpx alone

The names are the code’s names: the plugin sets MCPX_SESSION_ID (plugin/opencode/mcpx-session.ts), MCPX_CALL_ID and MCPX_PARENT_SESSION_ID, and those are the names the session and parent-session scopes read. (MCPX_SESSION without a suffix is a different, live variable: mcpx sets it for each script it runs. It is not the plugin’s variable and must not be conflated with it.)

Plugin to daemon

The portable core, plugin/opencode/mcpx/daemon.ts, imports nothing from opencode. Its DaemonClient / connect() finds a daemon by walking a ladder and stopping at the first answer (full design: plugin-discovery.md):

#rungapplies when
0an endpoint or socket named outright, by option or environmentconfigured, or remote — and it never falls back to a local daemon
1the choice remembered for this directory, still answeringevery session after the first
2the daemon-*.json info files the daemon publishesthe usual case; no binary needed
3GET /v1/resolve against any live daemontwo or more candidates
4spawn mcpx --json statusa binary exists and the daemons are too old for rung 3 — the only rung that runs the binary. backend: v1 does not suppress this spawn: the backend guard governs the tool-execution fallback, and discovery still spawns when no daemon is discoverable. Only bin: "" forbids spawning outright
5ask the user (toast, picker, or mcpx_daemon_select)still ambiguous, and someone is there
6the best candidate, with a warningheadless, or unanswered — the ladder never fails just because there are two

connect returns { client: undefined } rather than throwing: a plugin that fails to load because mcpx is not running has broken the editor for a tool the user may not even be using.

For authors of plugins for other harnesses

The core is harness-free on purpose; an adapter per harness supplies the rest. What an adapter must provide (docs/opencode-plugin.md §5):

  1. a directory, to resolve which daemon;
  2. a session identifier, to attribute and route;
  3. somewhere to put environment variables, or a way to wrap tool calls;
  4. a hook after a tool call, if timings are wanted;
  5. a way to show a line of text, if ambiguity is to be reported;
  6. optionally, a way to ask a question and a way to run a model.

Only (1) and (2) are required. Everything else degrades.

Configuration

Precedence is plugin option > environment variable > default. Options reach the plugin only when it is named in the opencode config as a two-element array — a plugin merely dropped into the plugin/ directory gets no options, so the environment variables are the way to configure a drop-in installation (see opencode v1 plugin).

The table below is derived from one declared list: the registry’s plugin.* settings, declared in internal/settings/plugin.go (pluginSettings()), which is also what mcpx settings reports and what the settings tests pin against the plugin’s sources in both directions. The two options that are not settings — endpoint and socket, which name a daemon rather than tune the plugin, and are read before any settings exist — are declared alongside the table, not in the registry. If an option is missing from this table, that is a bug in the table’s source list, not a detail to fix in the table: discoveryRetry went missing from the plugin README’s hand-maintained table exactly that way.

optionvariabledefaulteffect
endpoint (not a setting)MCPX_DAEMON_ENDPOINT, MCPX_ENDPOINT—a daemon named outright: http://host:port or unix:///path. Stops the ladder.
socket (not a setting)MCPX_SOCKET—a socket named outright. Stops the ladder.
binMCPX_PLUGIN_BINmcpxthe binary for rung 4. "" forbids spawning anything.
binArgsMCPX_PLUGIN_BIN_ARGS—arguments put in front of every plugin invocation of mcpx (the case this exists for is --config or --profile).
backendMCPX_PLUGIN_BACKENDautoauto prefers the socket and spawns only as a fallback; v1 refuses the spawn fallback for tool execution (discovery may still spawn when no daemon is discoverable — bin: "" is the way to forbid spawning entirely); cli never uses the socket.
discoveryRetry (environment only)MCPX_PLUGIN_DISCOVERY_RETRY60show long the plugin waits before looking for mcpx again after a failed lookup, so an editor without mcpx does not pay for a failed spawn on every keystroke. Declared in the registry, but the adapter reads it from the environment only — passing it as a plugin option is silently ignored.
toolTimingMCPX_PLUGIN_TOOL_TIMINGoffrecord opencode’s tool timings into mcpx’s log, so one mcpx stats covers both.
envMCPX_PLUGIN_ENVfullhow much session context is injected into each command’s environment: minimal, standard, full (the lists are under opencode v1 plugin).
instructionsMCPX_PLUGIN_INSTRUCTIONSoffadd mcpx usage guidance to the system prompt.
toolsMCPX_PLUGIN_TOOLSoffoffer mcpx_discover, mcpx_exec and mcpx_observe as tools the model can call.
rememberMCPX_PLUGIN_REMEMBERsessionhow long a chosen daemon stays chosen: session, until-gone, indefinite.
annotateMCPX_PLUGIN_ANNOTATEonadd one line to each mcpx tool result naming the daemon that answered, when more than one matched.
headlessMCPX_PLUGIN_HEADLESSautosuppress toasts, because nobody is looking at a screen. auto decides from the process shape.
daemonToolsMCPX_PLUGIN_DAEMON_TOOLSautooffer mcpx_daemon_status, mcpx_daemon_select and mcpx_daemon_forget. auto offers them when tools is on, or when discovery found more than one daemon.

MCPX_STATE_DIR and XDG_STATE_HOME are read too, because that is where the daemon publishes its info files and where the remembered choice is kept. They are mcpx’s variables, not the plugin’s. The full configuration reference is configuration.md.

opencode v1 plugin

This section is about the harness plugin for opencode v1 (1.18.31) — the one plugin that ships today. It lives in plugin/opencode/; the install and reference material is plugin/opencode/README.md, and the design is opencode-plugin.md.

What ships

filewhat it is for
plugin/opencode/mcpx-session.tsthe opencode adapter: hooks, tools, toasts, and the session id. The only file opencode loads as a plugin.
plugin/opencode/mcpx/daemon.tsthe portable core: the daemon HTTP client, the discovery ladder, and the remembered-choice file. Imports nothing from opencode.
plugin/opencode/mcpx-tui.tsxoptional TUI plugin: an arrow-key picker for the daemon. A different realm with a different API, installed separately.
plugin/opencode/skills/four skills: mcpx-basics, mcpx-observability, mcpx-browser, mcpx-daemon. The same directories are embedded in the mcpx binary and served over MCP, so an MCP host can load them without copying anything.

Install

By hand, with cp -R — there is no packaged install, and the Nix configuration does not install the plugin:

mkdir -p ~/.config/opencode/plugin
cp -R mcpx-session.ts mcpx ~/.config/opencode/plugin/

# optional, and a separate artifact: the arrow-key daemon picker
cp mcpx-tui.tsx ~/.config/opencode/plugin/

# skills are separate again
cp -R skills/mcpx-daemon ~/.config/opencode/skills/

(run from plugin/opencode/). mcpx/ must stay a subdirectory: opencode v1 loads every file directly in plugin/ and calls each of its exports as a plugin, so daemon.ts beside mcpx-session.ts would have its helpers called as plugins. The glob is not recursive, so one level down the core is only ever imported.

To pass options, name the plugin in opencode’s config as a two-element array; a drop-in file gets none:

{
  "plugin": [
    ["./.opencode/plugin/mcpx-session.ts", { "tools": true, "backend": "v1" }]
  ]
}

Every option also has the environment variable in the table under plugin interface and config, which is the way to configure a drop-in installation.

What gets injected

How much goes into each shell command’s environment is the env option:

  • minimal: MCPX_SESSION_ID only.
  • standard adds what the hook is handed directly, what the plugin knew at startup, and the daemon this session settled on — MCPX_CALL_ID, MCPX_PROJECT_ID, the worktree and harness variables, and MCPX_DAEMON_ENDPOINT, so a shell command that runs mcpx talks to the same daemon the plugin does instead of walking its own ladder.
  • full (the default) adds what needs a lookup, done once per session and cached: MCPX_PARENT_SESSION_ID, the session title, directory, version, depth, creation time and age, and the full trace ancestry in MCPX_TRACE_IDS.

Environment variables are not context: the model never sees them, so anything cheap goes in. The full list, with who reads each variable, is in environment.md.

The tools

With tools: true, the plugin offers mcpx_discover, mcpx_exec and mcpx_observe, each speaking the daemon API first and falling back to the binary only when there is no daemon (never under backend: v1). They are off by default because an agent with a shell can already run mcpx, and a tool definition costs context on every request whether or not it is used. Turn them on when the agent has no shell, when you want mcpx calls visible as tool calls in the transcript, or when a model keeps forgetting mcpx exists.

The daemon tools — mcpx_daemon_status, mcpx_daemon_select, mcpx_daemon_forget — answer the which-daemon question when discovery found more than one. They gate nothing; work proceeds on the best candidate with a warning until somebody chooses.

Two more options, both off by default for the same context-cost reason: instructions adds four lines of mcpx usage guidance to the system prompt, paid on every request for the life of the session; toolTiming records every opencode tool call into mcpx’s log (a 0.09 ms POST over the socket; with no daemon the record is dropped, never spawned — a timing is not worth 23 ms).

The TUI picker

mcpx-tui.tsx adds one palette command, mcpx: choose daemon: each daemon’s project, start time, server count and version, then how long the choice should stick. It is a separate plugin because the TUI runs in a different realm with a different API — a v1 module may export server or tui, never both — and a choice made there reaches the server plugin through the remembered-choice file.

Interplay with opencode’s own code mode

None, and none needed. opencode v1 ships its own code mode behind OPENCODE_EXPERIMENTAL_CODE_MODE, which moves MCP tools out of the model’s tool list; the mcpx plugin does not conflict with it, and the plugin needs nothing in opencode’s mcp config — it reaches the daemon itself, over the socket.

opencode v2 plugin

Status: not built. This section is about the harness plugin for opencode v2 (2.0.3), and it is a status section, not a feature description: v1 plugins do not load in v2 at all, so the plugin needs a rewrite, not a port. The work is tracked in the repository’s issues (#47 in the original numbering; see #1855), and the full design is in opencode-plugin.md §4 and the compendium it links.

On opencode v2 today:

  1. Shell + AGENTS.md works, with no session id — so session-scoped servers are isolated per call rather than per session.
  2. Or add mcpx as an MCP server: mcpx serve --transport stdio in v2’s mcp config. One caveat: v2 defaults MCP servers to code mode, so mcpx’s own tools end up wrapped inside v2’s code mode; whether v2’s code-mode defaults recognise mcpx and turn that off for it is unknown.

What the rewrite changes, when it happens:

  • Unnecessary: the mcpx/ subdirectory (only default is read), hand- namespacing tool names, a state file for the remembered daemon (ctx.storage exists), and the per-command shell.env hook (the session environment API sets a session’s shell environment once per session).
  • Possible: answering MCP sampling with ctx.generate.text; registering MCP servers per session, so mcpx could hand opencode a filtered server set; putting a budgeted catalog into the session context hook instead of the system prompt; seeing tool failures in the after-hook, not just successes.
  • Breaks: the plugin itself, Bun’s $ (gone from the v2 plugin API — the other reason the plugin must not need the binary), sessionID in the shell hook (its replacement carries no session id), and MCP SSE transport.
  • The same: the TUI SDK surface, no isolation (plugins run in the host’s realm), and Bun’s fetch(url, {unix}) — so the socket fast path survives the rewrite.

One v1 quirk worth knowing while both versions are in use: v1.18.31 runs both plugin systems at once, so a v2 plugin file dropped into a v1 plugin/ directory is loaded by the v2 host and rejected, harmlessly, by the v1 loader.

vibeMcp

Placeholder: what “vibeMcp” means is an open question for Drew (open question 1 on #1855) — this anchor holds the name, and no meaning is guessed here until he answers.

Proposals

Open design questions and answers. Nothing here is committed to; the point is to make the decisions visible before they are made. Shipped work moves to story.md under the feature list.


Answers about what exists today

Before proposing anything, the current state, verified in the code.

Shared servers are started lazily and do shut down. Nothing starts at daemon boot except the schema warm, which starts each server once to read tools/list and lets it idle out. ReapIdle runs on a 30-second timer against each server’s idleTimeout (default 5m), and a shared instance is reaped when len(instances) > Min, where Min defaults to 0. So the steady state for an unused server is zero processes, and the next call pays a cold start.

Today’s session mode is per-script-run. mcpx run mints a fresh key per invocation and releases it when the process exits. --session <key> overrides it, which means “named” already half-exists. Nothing is aware of an opencode session, a repo, or a worktree.

There are no ports. stdio servers are pipes; only url servers have a port and it is theirs. The predecessor’s port-assignment machinery was deleted on purpose.

Config is first-file-wins with no merge. A project .mcpx.json replaces the user config entirely rather than adding to it. See below — this is wrong.

Server-provided instructions are parsed and discarded. initialize returns an instructions string; mcpx reads it into a struct field nothing consumes. Descriptions and namespaces come only from config.

Scripts have no declared contract. stdout and stderr both pass through, the exit code propagates, and there is no log helper, no structured return, and no entry-point convention.


Modes and scopes — shipped

id:           modes-and-scopes-shipped
status:       shipped

Built. See Sharing and scope in the feature list. The retired mode key is now a hard error carrying a migration hint rather than a silent default. What follows is the original proposal, kept for the reasoning.

impact:  breaking config change

The current single mode axis conflates two independent questions. The proposal is to split them.

sharing — how many callers may use one process at a time.

valuemeaning
sharedmany concurrent callers per process; MCP multiplexes by JSON-RPC id
exclusiveone caller at a time; others queue

scope — what key selects a process. One process per distinct key value.

scopekeywho computes it
globalconstantmcpx
repogit rev-parse --git-common-dirmcpx
worktreegit rev-parse --show-toplevelmcpx
cwdworking directorymcpx
session$MCPX_SESSION_ID, else the run keycaller
parent-session$MCPX_PARENT_SESSION_ID, else $MCPX_SESSION_IDcaller
callfresh per invocationmcpx
named:<x>the literal stringcaller
pid:<n>the pidcaller

Today’s modes become aliases, so existing configs keep working:

todaybecomes
sharedsharing: shared, scope: global
pooledsharing: exclusive, scope: call
sessionsharing: exclusive, scope: call

That last row is the honest mapping: today’s session is per-call.

The part mcpx cannot do alone

scope: session needs an identifier mcpx has no way to discover. A subagent and its parent are different processes with different pids and the same cwd; only the agent host knows they differ.

So the contract is an environment variable. Callers that know their session export MCPX_SESSION_ID and, if nested, MCPX_PARENT_SESSION_ID. Everything else — repo, worktree, cwd, call — mcpx computes itself and works with no cooperation.

For opencode specifically that means either a plugin that adds the variable to the shell environment (v1 shell.env, v2 shell.hook["create.before"]), or an instruction telling the agent to pass --session. The plugin is better because it cannot be forgotten.

Recommendation for the current servers: chrome-devtools at sharing: exclusive, scope: session once the variable exists, scope: call until then. Everything else stays sharing: shared, scope: global.

Lifetime

A scope needs a teardown rule or it leaks.

scopereleased when
callthe process exits (today)
cwd, repo, worktree, globalidle timeout
session, namedidle timeout, or mcpx session stop <key>
pidthe watched pid exits

pid:<n> is worth building early and is cheap: poll kill(pid, 0) on the existing 30-second reaper tick. It gives an agent host a way to say “this browser belongs to that process” without any protocol.


Session introspection

id:           session-introspection
status:       proposed
depends: modes and scopes

A lease should be an addressable object.

mcpx session ls                     scope, key, server, pid, uptime, idle, calls
mcpx session show <key>
mcpx session stop <key>
mcpx session rename <key> <name>
mcpx session set <key> --idle-timeout 30m
mcpx session watch <key>            follow its calls live

Recorded per lease at creation: created-at, scope, key, server, instance id, child pid, creator pid, creator cwd, creator hostname, creator username, MCPX_SESSION_ID and parent if supplied, and the mcpx version. Recorded per attach and detach: timestamp, pid, cwd, and how it ended (released, idle, killed, pid-gone).

The same data behind mcpx --json session ls and as a client import, so a script can look at its own lease and adjust its own timeout.


Logging

id:           logging
status:       proposed

Two stores, because they answer different questions.

JSONL files, one per daemon, at $XDG_STATE_HOME/mcpx/logs/<config-hash>/<date>.jsonl. Append-only, buffered, flushed on a 200 ms tick and on shutdown. One object per line with ts, level, server, instance, session, scope, tool, dur_ms, msg, plus free-form fields. Cheap to write, trivially greppable, survives anything that does not SIGKILL.

A SQLite index at .../logs/index.db with one row per call rather than per log line — server, tool, session, start, duration, ok, bytes in and out. That is the table mcpx stats reads. Keeping it call-grained rather than line-grained is what keeps it small enough to stay fast.

mcpx log   --since 1h --server chrome-devtools --level warn --session <key>
mcpx log   --follow
mcpx stats --since 24h --by server      calls, p50/p95/p99, failure rate

Levels resolve most-specific first: per-session, then per-server, then global, from config or MCPX_LOG_LEVEL. Default info to stderr, debug to file. Erring toward more logs is right for the file and wrong for stderr — stderr is shared with the script’s own diagnostics and an agent reads it.

On SIGTERM the buffer flushes before listeners close, the same ordering fix already made for the schema cache. mcpx stop --force skips the flush.

Is there a standalone mode? Today, no: every command talks to a daemon and auto-starts one. That is right for pooling, which needs a process that outlives the call. A --standalone flag that runs the pools in-process for a single command is worth having for CI, where a lingering daemon is a nuisance.


Configuration — merge shipped

id:           configuration-merge-shipped
status:       shipped

The chain now merges, nearest first, per server name. A project adds servers instead of hiding the user’s. mcpx config --sources shows every contributing file and which one defined each server.

One consequence worth recording: the daemon fingerprint now hashes every source, not just the nearest. Two projects with byte-identical local configs can still inherit different servers from different parents, and keying on the nearest file alone would have given one project the other’s servers.

The rest of this entry is the original proposal.

impact:  behaviour change

Merge the chain instead of taking the first file. Today a project config hides every user-level server, which makes “add one server for this repo” impossible without copying the whole file. Proposed: walk the whole chain, nearest first, and merge by server name. A nearer file overrides a server of the same name and adds new ones; "disabled": true turns off an inherited one. pool merges key by key.

mcpx config --sources prints the chain with provenance per server, because a merge you cannot inspect is worse than no merge.

Scripts already continue the whole chain, nearest wins per name. That is the behaviour config should match.

.config/mcpx is now checked everywhere .mcpx is, at higher precedence, with a warning when both exist in one directory. (Shipped.)

Per-user versus global. The daemon is already keyed to a config file, and a config file is reached through $HOME, so “global” means “this user’s default config”. A true per-host config is /etc/mcpx/config.json, already last on the chain. No third concept is needed; the docs should just say per-user and stop saying global.

Name collisions. Servers aggregate by name. Two servers with the same name in different files merge; two servers with different names that map to one namespace is already a hard error that names both.

Scale. Discovery is a map read regardless of server count. The real limit is process count, sum(max) across servers times the number of daemons. Proposed: a daemon-wide maxInstances with LRU eviction of idle leases, so a pathological config degrades into slower leasing rather than fork-bombing.


The script contract

id:           the-script-contract
status:       proposed

Underspecified today and worth pinning down.

Streams. stdout is the result. stderr is diagnostics. A script that prints nothing to stdout returns nothing.

A log helper, exported by the client, writing structured lines to stderr at a level, so script output and script logging stop competing:

import { log } from "./mcpx-client.ts";
log.debug("snapshot", { bytes: snapshot.length });
log.warn("retrying", { attempt });

Returning a value. If a module has a default export that is a function, mcpx calls it with parsed arguments and JSON-prints whatever it returns; if it does not, top-level await runs and stdout is the result. That makes a file both importable and runnable without ceremony:

export function helper() {
  /* importable */
}
export default async function main(args: string[]) {
  return { ok: true }; // printed as JSON
}

mcpx run <name> --export <fn> calls a named export instead of the default, which covers “one file, several entry points”.

Arguments already arrive as Deno.args / process.argv. Proposed addition: --json-args '<json>' parsed and passed as the first parameter to the entry function, so callers do not have to serialise through argv.

mcpx run --json wraps the whole thing — stdout, stderr, exit code, duration, tool calls made — in one envelope for programmatic callers.


Modules and libraries

id:           modules-and-libraries
status:       proposed

Update (#1913): the dependency-configuration half of this area has shipped — the deps.* settings and paths.deps resolve where a script’s packages come from (script specifiers, explicit request, nearest project manifest, the configuration directory’s own packages, then the host), the set travels on ExecOptions as data, and the runner materialises it in the exec workdir per runtime. What remains proposed here is only the shared-code half below: a declared lib/ and the @lib/ import map.

Scripts run on a real runtime, so imports already work: npm:, jsr:, https: on Deno, node_modules on bun and node. What is missing is a declared place to put shared code.

Proposed: .config/mcpx/lib/ beside scripts/, on the same upward search, and a generated import map aliasing @lib/ to the nearest one. A project gets its own lib automatically; a user-level lib is the fallback; a project can shadow a user module by name, which is the same rule as scripts.

Isolation. Separate import maps per project give natural isolation between projects. Within one process there is none — a script can monkey-patch anything it imports. That is inherent to running on a real runtime with no sandbox, which is a deliberate choice. If isolation ever matters more than capability, that is the argument for v2-style interpretation, not for bolting a sandbox on.

Version overrides fall out of the import map: a project pins whatever it wants and the user-level pin is irrelevant to it.


Types

id:           types
status:       proposed

mcpx types <ns>.<tool> — emit one function with its argument and return types expanded recursively, plus the namespace’s description. Small, obviously useful, no design risk.

Surface the server’s own instructions. initialize returns an instructions string that mcpx currently discards. chrome-devtools-mcp almost certainly explains pageId there. Proposed: cache it and emit it as a namespace-level doc comment in types output. This is the cheapest fix for the “how do I get a pageId” problem and it is nearly free.

Producer inference. The real problem: pageId: number does not say to call list_pages first, and including every tool that mentions pageId is all 27.

Heuristic worth trying: for a required parameter P of tool T, a producer is a tool that does not require P and whose name or description contains P’s noun. For pageId that selects list_pages, new_page, select_page — three, not twenty-seven. Emit them as a comment:

/** Targets a specific page by ID. @see list_pages, new_page, select_page */
pageId: number;

It is a guess and should be labelled as one, but a wrong hint costs a few tokens and a missing hint costs a failed call and a retry.

Context-budgeted catalog. mcpx types is all-or-nothing: 4,402 tokens for chrome_devtools whether three tools are needed or all 27.

opencode v2 solves this with a round-robin fit, worth copying exactly: every namespace is always listed with its tool count, so nothing is invisible; each namespace’s signatures are ranked cheapest first; then it loops over namespaces taking one signature each per pass until the token budget is spent. The effect is that a namespace with three small tools shows all three, while a 27-tool namespace shows as many as fit, and no single large namespace can crowd out the others. v2’s budget is 2,000 tokens, which on the current servers shows 33 of 43 tools.

Proposed: mcpx catalog [--budget 2000], emitting one paste-ready block whose size is bounded no matter how many servers are configured. Pair it with search for the tools that did not fit.

OpenAPI. fromSpec is v2’s, and mcpx does not use it — there is no OpenAPI support here at all. The proposal is to build an equivalent: compile an OpenAPI 3.x document into a namespace of tools. It composes with pools exactly as an MCP server does, and it is a large amount of reach for a contained amount of code.


Round-robin catalogue budgeting

id:           round-robin-catalogue-budgeting
status:       proposed
effort:  small; the ranking already exists in a different form

mcpx types <ns> is all-or-nothing. mcpx catalog [--budget N] would fit a paste-ready block to a token budget instead.

The algorithm, which is opencode v2’s and worth copying exactly:

  1. List every namespace with its tool count first. Nothing is ever invisible, even at budget zero.
  2. Rank each namespace’s signatures cheapest first, where cost is len(line)/4.
  3. Loop namespaces, taking one signature per namespace per pass, until the budget is spent.

The rotation is load-bearing. Ranking globally by cost lets one namespace’s cheap tools crowd out everything else; taking whole namespaces in turn lets a 27-tool namespace eat the budget. Per-pass rotation gives a small namespace all its tools and a large one as many as fit.

Ranking is by cost, not relevance. There is no similarity search and no model involved: it is sort by length, ties broken by path. That is a deliberate difference from mcpx search, which is relevance-ranked by substring scoring (exact 100, prefix 60, contains 40, function path 30, namespace 20, description 10) with an any-term fallback. Neither uses embeddings or an external library.

Keep whole-namespace types as well. They answer different questions: catalog is “what is available, within a budget”, types is “I have chosen this namespace, give me all of it”. An agent that knows it needs Chrome should not be rationed. Deleting types in favour of catalog --budget huge would also make the common case require a flag.

Open question worth trying: mcpx catalog --budget 2000 --bias screenshot, where a query biases which signatures win their pass. That merges the two ranking systems and may be better than either.

2026-09-27T07:00:00-05:00

Protocol adapters, including OpenAPI

id:           protocol-adapters-including-openapi
status:       proposed
effort:  medium for the interface, small per adapter

A namespace is currently always an MCP server. The generalisation is an adapter interface: something that can list tools with JSON Schemas and call one. MCP is the first implementation; OpenAPI would be the second.

Yes, an OpenAPI service could be imported as a namespace. Its operations already carry typed parameters and responses, which is exactly what the existing JSON-Schema-to-TypeScript compiler consumes, and it would inherit pooling, scoping, types and run unchanged.

MCP stays native. Nothing about this replaces it.

Emitting an OpenAPI document per namespace is the interesting inverse and is cheap once schemas are cached: it would let non-agent consumers — a dashboard, a test harness, another language’s client generator — use the same servers without speaking MCP. Worth doing after import, not before.

Would you ever not want it available? Two honest arguments against:

  • Every transport is surface area. An OpenAPI document can describe authentication, content negotiation and streaming semantics that MCP does not, and partially supporting them is worse than not offering it.
  • A script can already fetch() an HTTP API directly. The adapter buys typed signatures and pooling, which matters for a large API and is pure overhead for a small one.

So: build the adapter interface, because it is the right shape regardless; treat OpenAPI as the proof that the interface is general; and do not present it as a headline. Reordered to the bottom of the shipped-work queue.

2026-09-27T07:00:00-05:00

Per-instance argument templating

id:           per-instance-argument-templating
status:       proposed
effort:  small

Pooled instances currently get identical argv. Some servers need each instance to differ — a distinct port, profile directory, or upstream URL.

"chrome-attached": {
  "command": "chrome-devtools-mcp",
  "args": ["--browserUrl", "http://127.0.0.1:{{alloc.port}}"],
  "mcpx": {
    "scope": "session", "max": 4,
    "allocate": { "port": { "range": [9222, 9299] } }
  }
}

Substitutions available to args, env and url: {{instance.id}}, {{instance.index}}, {{key}}, {{alloc.<name>}}, {{tmpdir}}.

This is not needed for Chrome as configured today. chrome-devtools-mcp takes no listen port: with --isolated it launches its own browser with a temporary profile and an ephemeral debugging port of its own choosing. That is precisely why mcpx has no port machinery, and why the predecessor’s port-assignment code was solving a problem this server does not have.

It becomes necessary for --browserUrl / --wsEndpoint, where each instance must attach to a different already-running Chrome. And a real allocator has to survive the time-of-check-to-time-of-use gap that sank the earlier attempt: bind the port, hold the listener, pass it down, release on start.

2026-09-27T07:00:00-05:00

Scope policy

id:           scope-policy
status:       proposed
effort:  small

Scope is currently set per server and cannot be constrained or overridden.

"pool": {
  "scope": "session",
  "scopePolicy": {
    "allow": ["session", "parent-session", "call"],
    "allowOverride": ["flag", "script"]
  }
}

allow restricts what a per-server block may choose. allowOverride says who may change it at call time: flag for mcpx run --scope, script for a client call, none to pin it. A denied override is an error naming the policy, never a silent downgrade.

The case for it is a shared machine or a checked-in config where one project should not be able to widen a browser’s scope and start sharing state with another.

2026-09-27T07:00:00-05:00

Named sessions and keepalive control

id:           named-sessions-and-keepalive-control
status:       proposed
effort:  medium

A key exists today and can be supplied with --session, which is already naming by another word. Missing: discovery, persistence and adjustment.

mcpx session ls
mcpx session name <key> <name>        name one that started anonymous
mcpx session set <name> --idle 30m
mcpx session stop <name>
mcpx session show <name>

Naming an anonymous session is the interesting one, because an ephemeral key is currently torn down when its caller exits. Naming must flip that bit: a named session outlives its creator by definition.

Per-scope keepalive defaults. One idleTimeout for everything is wrong, because the scopes have different natural lifetimes:

scopedefaultwhy
global30mexpensive to lose, cheap to hold
repo, worktree15mtracks a working session
cwd10mnarrower, more of them
session, parent-session10magent turns have gaps
pidnonereleased when the pid exits
callnonereleased when the caller exits

Overridable in config per server, and at runtime per session.

2026-09-27T07:00:00-05:00

Health-based and webhook reaping

id:           health-based-and-webhook-reaping
status:       proposed
effort:  small for health, medium for webhooks

Health check. A server that has wedged without exiting currently occupies its slot until idle timeout. "health": { "url": "...", "interval": "30s", "expect": 200 } would reap on a failing probe. MCP already defines ping, so the first version should use that rather than HTTP — it needs no configuration and works for every server.

Webhook. Two directions, and they are different features:

  • Outbound: mcpx notifies a URL on lifecycle events. This is just the event stream with an HTTP sink and is easy.
  • Inbound: something tells mcpx to reap. That is a control API needing authentication, and mcpx session stop over the existing socket already does the job for anything local.

Recommendation: ping-based health first, outbound events second, inbound webhooks only if a concrete need appears.

2026-09-27T07:00:00-05:00

Structured logging

id:           structured-logging
status:       proposed
effort:  medium
supersedes: the earlier "Logging" entry

Consolidates the whole logging intake. Nothing here exists yet; the daemon prints unstructured lines to stderr and scripts have no logging helper at all.

Records

One record type everywhere, emitted by daemon and scripts alike. Go’s slog is the model, and its slog.Handler interface is the right seam for formats.

Every record is enriched by the daemon with ambient context the caller did not have to supply: session id, parent session, scope, resolved key, server, instance, tool, pid, cwd, config path, mcpx version, session start, script start, elapsed. A script logs log.info("done") and the record that lands has all of it.

Templates

A message keeps both forms:

log.info("status: {status}", { status: "ok" });
{ "ts": "...", "level": "info", "msg": "status: ok", "template": "status: {status}", "status": "ok" }

msg interpolated for humans, template preserved for grouping records that differ only in values. That is what makes “how often did this line fire” answerable. Named {placeholders} over %s because they map onto the attributes without positional ambiguity — the same choice Serilog and structlog make.

A bare string with no attributes is still wrapped into the same shape, so there is one record type and no second path.

Formats

--format on every command that emits records:

formatshape
jsonone object per line
json-prettyindented, for reading
logfmtts=… level=… msg=…
text (default)HH:MM:SS LEVEL msg key=value
compactLEVEL msg, attributes interpolated only
baremessage only

text interpolates the template and appends attributes not consumed by it. compact and bare drop the timestamp.

Everything as JSON

--json already exists on several commands and should exist on all of them, with every human table having a JSON counterpart. Scripts should get the same: mcpx run --json wrapping stdout, stderr, exit code, duration and the tool calls made.

Storage

JSONL per daemon under $XDG_STATE_HOME/mcpx/logs/, buffered and flushed on a tick and at shutdown, plus a SQLite index of one row per call rather than per line. Call-grained is what keeps mcpx stats fast; line-grained would not.

Default info to stderr, debug to file. More logs to the file is right; more to stderr is not, because an agent reads stderr.

2026-09-27T07:00:00-05:00

Harness integration and installation

id:           harness-integration-and-installation
status:       proposed
effort:  large; several independent pieces

Consolidates the distribution intake.

Division of labour

Three things a harness needs, and they should not be bundled:

  1. Instructions — the AGENTS.md text. Any harness that reads a markdown instructions directory can use it with no code.
  2. Session identity — exporting MCPX_SESSION_ID and MCPX_PARENT_SESSION_ID into the shell environment. This genuinely needs a plugin, and it is what makes scope: session work.
  3. Tool surface — optional. A harness could expose mcpx as a tool rather than relying on shell access.

Only (2) requires code. Keeping them separate means a harness with no plugin support still gets most of the value.

Avoiding double-injection

If a plugin injects instructions and an instructions file exists, the text lands twice. The plugin should look for a marker — a known first line in the file — and skip injection when it finds one. Detection beats configuration because it cannot drift.

Install command

mcpx install opencode|opencode2|claude|codex [--dry-run] [--platform nix|nix-flake|darwin|darwin-brew]
mcpx install --list

Writes instructions, plugin and config edits for that harness. Hash-matching is the important part: record a hash of what was written, and on the next install update the file only when it still matches. A user-modified file is reported, never overwritten.

Daemon-side auto-update of harness configs is listed in the intake and should be declined, or made strictly opt-in. A background process editing another tool’s configuration is surprising in a way that is hard to debug.

Documents

  • An install document fetchable with curl.
  • A setup document written for an agent — different text, assuming tool access and no human. mcpx setup-prompt emits it. This is the genuinely novel piece: the artefact is a prompt, not a script.

2026-09-27T07:00:00-05:00

Packaging and service management

id:           packaging-and-service-management
status:       proposed
effort:  medium

A nix flake package exists. Missing: NixOS module, darwin module (one existed and was dropped when the package moved into the nix repo), Homebrew formula, and a plain installer for non-nix systems.

Service installation. mcpx service install|uninstall|status writing a launchd plist or systemd user unit.

Never with sudo. The daemon is a per-user process owning per-user MCP servers; a system-wide unit would run servers as the wrong user and share state between users. A user-level unit needs no elevation, and an installer that asks for a password to do something that does not require one teaches a bad reflex.

Autostart. Both, and they already coexist: any command starts a daemon on demand today, and a service unit only means the schema cache is warm before the first agent asks. Keep on-demand as the supported path so that nothing requires installation.

2026-09-27T07:00:00-05:00

Transport, addressing and scale

id:           transport-addressing-and-scale
status:       proposed
effort:  small for addressing, unknown for the rest

What exists today. The daemon serves HTTP over two listeners: a unix socket at $XDG_STATE_HOME/mcpx/daemon-<hash>.sock for the CLI, and 127.0.0.1 on an ephemeral port for generated script clients, because fetch() over a unix socket is not portable across deno, bun and node. The port is chosen by the OS, so there is nothing to collide with, and it is published in a daemon record file. Not stdio, not named pipes.

Multiple daemons already run, one per config fingerprint, each with its own socket. mcpx daemons lists them.

Addressing. --server accepting example.com, example.com:8008, :42 or a unix path would let the CLI drive a remote daemon. The blocker is not parsing but authentication and trust, which loopback currently provides for free. Worth specifying before building.

Correction, 2026-10-30: the remote half of this was specified and built — as an identity transport, not raw addressing. Since #1967 the daemon can embed a Tailscale node (tsnet, internal/daemon/transport_tsnet.go) and be reached by its tailnet identity (a MagicDNS name) from another network, with the tailnet providing exactly the authentication and trust this entry named as the blocker. Generic internet addressing (example.com:8008 and friends) remains unbuilt and unendorsed.

Load. Untested beyond 12 concurrent runs. Discovery is a map read and should be flat; the interesting limits are process count and the reaper’s O(instances) sweep. A benchmark should come before any optimisation.

Go versus Rust. No reason to move. The workload is process supervision and JSON, where the runtime is idle most of the time and goroutines map onto the problem directly. Rust would buy a smaller binary and no GC pauses, neither of which is a constraint here, at the cost of a rewrite and harder async process handling. Revisit only if profiling finds something the GC is responsible for.

2026-09-27T07:00:00-05:00

Name

id:           name
status:       proposed
effort:  none, but it gets more expensive every week

mcpx reads as “MCP extended” and is already in the docs, the binary, the config key and the environment variables.

Alternatives worth a moment: xmcp (same idea, scans worse aloud); mcpsh (says “shell”, which is the actual pitch); tsx-adjacent names (taken); conduit, broker, switchboard (descriptive, generic, unsearchable).

Honest assessment: mcpx is fine and the rename cost only grows. The one argument for changing is that the tool’s distinguishing feature is shell access to MCP, and mcpsh says that. Not worth the churn unless it is decided before anything publishes.

2026-09-27T07:00:00-05:00

Parity tracking

id:           parity-tracking
status:       proposed
effort:  small, ongoing

opencode v2 is the closest comparable and moves fast. A short table in OPENCODE-V2.md listing each capability and whether mcpx has an equivalent, reviewed per release, is cheap and prevents drift being noticed only when it is expensive.

Other systems worth reading for concepts rather than parity: Claude Code’s skills and hooks, Codex’s sandboxing, and the several MCP-adjacent efforts that expose tool catalogues as code. The specific thing to look for is how each keeps a large tool surface out of context, because that is the problem this tool exists to solve.

2026-09-27T07:00:00-05:00

Extra discovery paths

id:           extra-discovery-paths
status:       proposed
effort:  small

Config and scripts are searched upward from the working directory, then the user directory. Missing: a way to add directories to that chain.

--override-paths <dir|file>[,...]     tried before everything
--fallback-paths <dir|file>[,...]     tried after everything
MCPX_OVERRIDE_PATHS / MCPX_FALLBACK_PATHS

Two flags rather than one plus an ordering rule, because the two uses are genuinely different. An override is “for this invocation, use these”; a fallback is “if nothing local matches, look here too” — a shared team directory, for instance. Collapsing them into one list with a position argument makes every call site state a position it does not care about.

Accepting files and not just directories lets a single script be injected without building a directory around it.

Aliasing a file to a different name was raised alongside this and is probably the wrong shape. Shadowing already resolves name conflicts by precedence, and mcpx scripts already shows what is shadowed. An alias adds a second mechanism for the same job, and a second place to look when a name resolves to something unexpected. The case it would uniquely solve — wanting both of two same-named scripts — is better served by naming one of them differently, which the author controls.

2026-09-27T08:00:00-05:00

id:           better-tool-search
status:       partial
         and --explain are in; typo tolerance (on #1904's shared near-miss
         matcher) and concept similarity remain proposed
effort:  small to large depending on how far it goes
note:         slice 1 shipped 2026-10-30 (#1923) — tags, word-splitting/stemming

Search scoring today: exact 100, prefix 60, tag 50, contains 40, a split/stemmed name word 40, function path 30, namespace 20, description 10, all terms required with an any-term fallback. It is fast and has no dependencies, and it fails on typos and on concepts that share no substring with the tool name. mcpx search --explain prints the per-term breakdown behind a hit’s score, and --tag <tag> filters on config tags exactly (server tags, inherited by its tools, plus per-tool toolTags).

Three tiers, in increasing cost:

Tags. Cheap, exact, and author-controlled. "tags": ["browser", "visual"] per server, and mcpx search --tag browser. Also lets a config author fix a bad match without touching the server.

Typo tolerance. Levenshtein distance ≤ 2 against tool and namespace names only, applied after exact scoring returns nothing. Tool names are short and few, so this is microseconds and needs no index. screenshto finds take_screenshot.

Concept similarity. The expensive one. Either an embedding model — a new dependency, a download, and a cache to invalidate — or a hand-built synonym table, which is cheap but needs curating. Worth deferring until tags and typo tolerance are in and still insufficient.

Recommended order: tags, typos, then reassess. Most “I could not find the tool” cases are probably vocabulary, and tags fix vocabulary directly.

2026-09-27T08:00:00-05:00

Types improvements

id:           types-improvements
status:       proposed
effort:  small each

Beyond mcpx types <ns>.<tool>:

  • --no-instructions to omit server guidance. Shipped — fff returns 3,768 characters of it, which more than doubles that namespace’s output.
  • --depth N to control how far nested object types expand. Deeply nested schemas are most of the cost of a large namespace.
  • --only <tool>[,...] for several tools without the whole namespace.
  • Output-type rendering. outputSchema is fetched and ignored; every tool currently returns Promise<ToolResult>. Rendering it would make results typed rather than any.
  • --format ts|json|markdown, since the underlying model is structured and only the rendering is TypeScript.

2026-09-27T08:00:00-05:00

mcpx as a server

id:           mcpx-as-a-server
status:       proposed
effort:  medium

Correction, 2026-10-30: the re-export proposed here is still the open part — but “remote daemon” itself is no longer unbuilt. Since #1967 the daemon can embed a Tailscale node (tsnet, internal/daemon/transport_tsnet.go) and be reached by its tailnet identity, and the settings registry ships the client half (daemon.endpoint, daemon.address). What remains proposal is the code-mode MCP server / OpenAPI endpoint re-export above, not remote reachability as such.

Distinct from importing or emitting OpenAPI: this makes mcpx itself something other tools connect to.

As an MCP server. mcpx already aggregates servers, pools them and caches their schemas. Re-exporting that as one MCP server would let a harness with no shell access reach everything through a single connection, with pooling and scoping applied underneath. Two shapes, and they are different products:

  • Passthrough: every underlying tool re-exported under its namespace. Simple, and reintroduces exactly the context problem mcpx exists to avoid.
  • Code-mode: two tools, search and run, where run takes a script. This is what opencode v2 does internally, and it keeps the context win.

The second is the interesting one, and it makes mcpx usable from Claude Desktop or anything else that speaks MCP but cannot run a shell.

As an OpenAPI endpoint. The daemon already serves HTTP with a stable shape. Publishing an OpenAPI document for it, and optionally binding beyond loopback, would let non-MCP consumers use the same pooling.

Security. Everything today is loopback plus a 0600 unix socket, which is why there is no authentication. Binding wider needs a token at minimum, scoped per client, plus a decision about whether a remote caller may name a cwd — which currently determines repo and worktree keys and would otherwise be a way to reach another user’s instances.

Recommendation: build the code-mode MCP server first, loopback only, no authentication. Treat remote binding as a separate proposal with its own threat model.

2026-09-27T08:00:00-05:00

MCP protocol coverage

id:           mcp-protocol-coverage
status:       proposed
effort:  medium, several independent pieces

mcpx implements initialize, tools/list, tools/call, resources/list, resources/read and ping. The protocol defines more, and opencode v2 implements most of it. Meeting parity means, roughly in order of value:

  • instructions from initialize. Shipped. It was being parsed and discarded; fff alone returns 3.7 KB of tool-selection guidance.
  • prompts/list and prompts/get. Servers ship reusable prompts nobody can currently see.
  • Progress notifications. A long tool call is opaque today. The protocol has notifications/progress; surfacing it would let a script show progress.
  • Cancellation. notifications/cancelled is sent on timeout, and an incoming one stops the question it names (docs/protocol.md §4.1).
  • tools/list_changed. Servers announce catalogue changes; mcpx caches schemas until told to refresh, so it can be wrong until mcpx refresh.
  • Sampling. Servers can ask the client to run a model. Requires a model binding mcpx does not have and may never want.
  • Roots. Tells a server which directories it may touch. Cheap, and pairs naturally with worktree scope.
  • Elicitation. Servers ask the user a question mid-call. Needs a UI story for a tool with no UI; probably means failing the call with a clear message.
  • OAuth. Required by some hosted servers. The largest single piece.

2026-09-27T08:00:00-05:00

Chrome and stateful-server ergonomics

id:           chrome-and-stateful-server-ergonomics
status:       proposed
effort:  small each

Chrome is the hardest server to drive well and the lessons generalise.

pageId discovery. The concrete failure: click requires pageId and its description says only “Targets a specific page by ID”. Note that close_page does say “Call list_pages to list pages” — so the information exists in some schemas and not others, which is exactly the inconsistency a host should paper over. Chrome returns no instructions, so the instructions work does not fix this. Producer inference is the remaining answer.

Per-server prelude. A config-supplied snippet prepended to a namespace’s types output, so a user can state a convention the server does not. Half a day, fixes pageId immediately, and is a general escape hatch for every server with bad descriptions.

Recipes. A named script shipped per server — mcpx run chrome:screenshot <url> — turning the common sequence into one call. The generated flow from the story is already most of one.

--pageIdRouting is on by default and documented as being for concurrent sessions, which suggests Chrome expects several agents against one instance. Worth measuring against pooling before assuming pooling is required; pooling gives real isolation, but if routing is sufficient for a caller who does not mind sharing cookies, sharing: shared, scope: session may be cheaper.

Generalises to: any server whose required identifiers come from another call. A prelude and a recipe are the two cheap answers, and both are server-agnostic.

2026-09-27T08:00:00-05:00

Log context and enrichment layering

id:           log-context-and-enrichment-layering
status:       proposed
effort:  small

Enrichment happens in one place today: the runner folds a fixed map (session, cwd) into every record arriving from a script. That is enough for the values mcpx knows at launch and nothing else.

Two gaps.

A script cannot bind context. Shipped as log.with. What remains open is whether bindings should propagate without being passed, which on Node would mean AsyncLocalStorage and on Deno has no equivalent; a hidden global is the likely alternative and is worse than an argument. Original text follows.

Every call repeats what does not change:

log.info("scanned {file}", { file, run: runId, phase: "scan" });

Wanted: const scoped = log.with({ run: runId, phase: "scan" }), matching slog’s Logger.With. Bound attributes merge under per-call ones, so a call can still override.

Go-side context does not reach records. slog passes a context.Context into Handler.Handle, which is exactly the seam for request-scoped values, but nothing is read from it. A daemon handler could stash the session, the resolved scope key and the server name in the context once, and every record below it would carry them without being told.

Proposed layering, outermost first, each merging under the next:

  1. daemon context (server, scope, instance)
  2. runner enrichment (session, cwd, entry)
  3. script log.with bindings
  4. per-call attributes

The rule that makes it predictable is that the innermost wins, because it is the most specific statement about this one record.

2026-09-27T21:00:00-05:00

Reserved attribute names

id:           reserved-attribute-names
status:       partial
note:         shipped (collision handling), proposed (the rest)

A rendered record flattens attributes next to ts, level, msg and template. A user attribute with one of those names used to be dropped silently; it is now renamed to attr.<name>, so nothing is lost.

What remains open is whether mcpx should own a namespace instead. An underscore prefix (_kind, _session) or a dotted one (mcpx.session) would make the boundary obvious and remove the renaming rule entirely. The argument against is that every record then carries punctuation for a collision that almost never happens, and jq '.level' becomes jq '."mcpx.level"'.

Current view: renaming is the better trade while the reserved set is four names. If it grows – a log store will want id, run, seq – revisit, because at that point the collisions stop being hypothetical.

2026-09-27T21:00:00-05:00

Level filtering in the script — shipped

id:           level-filtering-in-the-script-shipped
status:       shipped

The script now knows the threshold and returns before encoding. Measured at 0.026 microseconds per filtered call, against 0.10 for the previous encode-and-write. Source capture moved to a per-level setting at the same time, defaulting to warn and above.

The original proposal follows.

effort:  small

A script serialises and writes every record; the threshold is applied in Go. log.debug() in a loop therefore costs a JSON encode and a write even when nothing will be printed.

Fix: pass the active level to the script as MCPX_LOG_LEVEL (already passed for other reasons) and have the client return early below it. About five lines, and it makes debug logging free to leave in.

The same argument applies harder to source capture, which costs ~4.9 us per call: checking the level first means an unprinted debug record costs nothing rather than a stack trace.

2026-09-27T21:00:00-05:00

Layered script prefixes

id:           layered-script-prefixes
status:       partial
note:         shipped (list + null splice), rejected (ancestor indexing)

mcpx exec generates a whole file, so it can wrap a snippet in lines from configuration. The question was how a nearer config should combine with a farther one.

Shipped: a list whose elements are strings or null. A null splices in whatever the setting inherited, at that position:

"script": { "prefix": ["import { h } from '@lib/h.ts';", null] }

A list with no null replaces outright. On the command line the marker is -: --prefix - --prefix 'const X = 1;' keeps the configured lines and adds one.

This is one concept doing both jobs. Inheritance is explicit and local: you can read a single file and know whether it extends or replaces, without knowing what is above it.

Rejected: integers addressing ancestors. The idea was 0 for the current inherited value, 1 for the parent’s, 2 for the grandparent’s. It is rejected because the number means nothing without the chain:

  • Chain depth varies by where a config was found. The same file has different ancestors in a repo, under ~/.config, and under /etc.
  • Moving a directory silently changes what 2 refers to.
  • It cannot be read locally. ["mine", 2] is unresolvable without reconstructing every file above it, which is the opposite of what a config should require.
  • The case it uniquely serves – “skip my parent but take my grandparent” – has not come up, and if it did, naming the thing wanted would be clearer than counting hops to it.

Null covers extend-or-replace, which is the distinction people actually make.

Corrected: prefixes do apply to file scripts, for effects rather than bindings. The first version rejected them outright, which conflated two claims. A prefix cannot bind anything inside a module, because ESM gives it its own scope. It can perfectly well act before and after one, because mcpx already generates a launcher around it.

So the launcher imports the module dynamically:

const script = { path, name, args, export };
<prefix lines>                 // runs first, for real
const mod = await import("./user.ts");
...
finally { <suffix lines> }     // runs even when the body throws

A static import would be hoisted and evaluate the module before any prefix line whatever the source order, which is why the dynamic form is required rather than merely tidier.

Prefix lines see script (path, name, args, export). Suffix lines also see result (ok, value, error, ms), and run in a finally, so cleanup happens on the failure path too. Both can set globals the script will read, which is the monkeypatching seam.

What remains impossible is a prefix declaring a const the script can see. That is ESM, not a limitation worth fighting, and the tests assert it stays impossible so nobody comes to depend on an accident.

A snippet is different: mcpx exec generates the whole file, so its prefix shares scope and can declare bindings the snippet uses.

2026-09-27T22:30:00-05:00

Launcher as a template with placeholders

id:           launcher-as-a-template-with-placeholders
status:       proposed
effort:  medium

The launcher is a fmt.Sprintf with five named phase slots. That is enough for “run something here”, and not enough for “reorder this” or “wrap the entry in my own try”. The next step is a real template.

Proposed: the launcher ships as a template file with named placeholders, and configuration may both fill them and reference them:

@globals  @before  @prefix  @import  @entry  @onSuccess  @onError  @suffix

A phase body could then say @prefix to splice another phase in, which is the mechanism behind the interesting cases: running a prefix twice, or moving the import after a guard.

Two rules make it safe:

  • References form a DAG. A cycle is rejected at generation time with the path that closed it, not discovered as a hang.
  • Each placeholder resolves once. Referencing one twice is an error unless allowRepeat names it, because the common case of a double reference is a mistake, and the rare deliberate one should have to say so.

Also wanted, and cheap: a minimal mode that emits the module import and nothing else – no globals, no console capture, no phases – for a script that wants the runtime and none of the harness.

The argument for doing this at all is that a launcher which is only half-configurable invites someone copying it out and maintaining a fork. The argument against doing it now is that five named phases have not yet been shown insufficient.

2026-09-28T01:00:00-05:00

Structured stack traces

id:           structured-stack-traces
status:       shipped

Error.prepareStackTrace is V8-specific and not in any standard, and was verified to work in Deno, Node and Bun. It hands back CallSite objects instead of a formatted string, so frames arrive as data:

{ "function": "boom", "file": ".../script.ts", "line": 9, "column": 22, "async": false }

Parsing the string form was the alternative and is strictly worse: it loses the async and native flags, and breaks on any path containing the characters the format uses as delimiters.

One trap worth recording. V8 memoises whatever prepareStackTrace returned the first time .stack is read. Asking for frames destroys the string; asking for the string destroys the frames. Both are wanted – the string for a human, the frames for a query – so frames are captured and the string is rendered from them. The first two attempts at this each lost one form, and a test now asserts both survive.

captureFrames(skip, limit) and errorFrames(err) are exposed to scripts.

2026-09-28T01:00:00-05:00

Defaults as an embedded layer

id:           defaults-as-an-embedded-layer
status:       shipped

Every default now lives in internal/defaults/defaults.json, embedded with go:embed, and is printable with mcpx config --defaults.

Before this they were four const blocks in four packages plus two literals in a ticker. That is the arrangement where “what is the idle timeout” takes a grep across the tree and still misses one – which is exactly what happened: a test asserting no package redeclares a default timeout found a five-minute literal in the daemon’s save ticker that three readings had walked past.

internal/defaults is a leaf with no mcpx imports, so logging can read it without depending on config. Parsing happens in a variable initialiser rather than init(), because Go evaluates package variables first and the init() version silently handed out zero values.

DisallowUnknownFields is set, so a misspelled key in defaults.json is a startup panic instead of a zero value discovered three layers down.

2026-09-28T02:00:00-05:00

Console fidelity

id:           console-fidelity
status:       shipped

All 25 of Deno’s console members are handled. The ones with a level become that level, log still reaches stdout, trace carries structured frames, clear resets indentation without erasing a durable log, and the devtools markers (profile, profileEnd, timeStamp) become file-only records rather than vanishing.

Two fidelity details that a test caught rather than review:

  • Names are preserved. console.info.name is still "info". Five methods had been assigned directly instead of through the helper that sets it.
  • Messages use the runtime’s own inspect, so console.info({a:1}) reads { a: 1 } exactly as it would have unwrapped. The JSON form is redundant – the structured value is already in args.

console.createTask() throws when called bare. So does Deno’s. The test compares against the unwrapped console rather than asserting nothing throws, because matching the original includes matching its failures.

2026-09-28T02:00:00-05:00

Protocol methods still missing

id:           protocol-methods-still-missing
status:       implemented
note:         audited 2026-09-29, method by method against all three schemas

An earlier audit assembled a feature list from the specification’s index page and probed a few URLs. That was not good enough: doing it properly – diffing every method: literal in schema.ts for 2025-06-18, 2025-11-25 and 2026-07-28 against what mcpx sends and serves – found more.

Still missing, in rough order of what would be noticed:

resources/subscribe / unsubscribe / notifications/resources/updated. A client watching a resource for changes. Present in every revision. mcpx declares subscribe: false, so it is honest, but a server publishing live data cannot tell mcpx when it changes.

resources/templates/list when serving. mcpx consumes templates from upstream servers and does not offer them onward, so a templated resource becomes invisible one hop down.

sampling/createMessage. A server asking the client to run a model completion. mcpx is not a model host, so the honest implementation is a pass-through to whatever is driving it – the same shape elicitation uses. Worth building when something asks. Shipped, then found unreachable: sampling was never declared, because the handler was installed after the handshake that declares capabilities. Fixed 2026-09-29, with roots, which the same ordering had broken.

notifications/elicitation/complete (2025-11-25+). A url-mode elicitation finishing out of band. Without it, a server that sends somebody to a browser has no way to say the flow completed, and the caller waits for the TTL.

subscriptions/listen and notifications/subscriptions/acknowledged (2026-07-28). The modern revisions generalised subscriptions into one mechanism. This is also the answer to “can a client subscribe to the daemon”, which mcpx currently has no way to do at all.

tasks/* (2025-11-25 core, an extension in 2026-07-28). Long-running work with polling and durable handles. An extension now, so optional by definition, but it is what a genuinely slow tool should use.

notifications/roots/list_changed. mcpx declares listChanged: false, so nothing is promised, but a root set that can change is more useful than one fixed at startup.

2026-09-29T00:30:00-05:00

Suggested order

Shipped since this document was written: sharing/scope split, pid-scoped lifetime, realpath-canonical keys, .config/mcpx lookup, named scripts, path globals.

  1. Server instructions surfaced in types. Shipped. Worth recording that it did not do what it was ranked first for: Chrome returns no instructions, so pageId is untouched. It turned out valuable for a different reason — fff returns 3.7 KB of tool-selection guidance that was being thrown away.
  2. mcpx types <ns>.<tool>, plus a per-server prelude. Shipped. 17,851 to 923 characters for one tool, and the prelude states where a pageId comes from, which is what instructions failed to supply.
  3. Config merge with --sources. Shipped.
  4. Script contract: log, default-export entry, run --json. Shipped.
  5. Structured logging. Shipped, minus the on-disk store and mcpx log / mcpx stats, which still want a JSONL file and a call-grained index.
  6. mcpx catalog --budget. Shipped, with --bias.
  7. Named sessions, mcpx session ls, per-scope keepalive.
  8. Scope policy.
  9. Harness integration and mcpx install.
  10. Packaging and mcpx service install.
  11. Ping-based health reaping.
  12. Producer inference for parameter hints.
  13. Per-instance argument templating and allocation.
  14. Protocol adapter interface, then OpenAPI import, then OpenAPI emit.

Deliberately unscheduled: remote addressing, Rust, rename.

Protocol: every revision, both directions

id:           protocol-every-revision-both-directions
since:        pre-release
created:      2026-09-29T20:00:00-05:00
last-updated: 2026-09-30T16:00:00-05:00
status:       standard
tags:         area:protocol
description:  what each revision defines, what mcpx accepts, what it sends,
              and why there is one HTTP server rather than two.

mcpx speaks all five revisions of MCP and sits in the middle of two of everything: two eras, two directions, two mechanisms for the same question. The rules that hold it together are short, and this document is mostly the table that falls out of them. The per-revision detail and its tests are in spec/; what the specification itself asks, independent of mcpx, is spec/server-obligations.md and spec/client-obligations.md.

Accept liberally, send conservatively. A server implementing a method beyond what the negotiated revision defines is not violating anything — it withholds nothing the revision requires, and a client that reached for the method would otherwise be told mcpx cannot do a thing it can. Sending is the opposite: a field, a notification or a server-initiated request that the peer did not negotiate and did not declare is a lie about what was agreed, and the failure is silent. Either the peer ignores it, or it rejects the frame, and nothing says which.

So mcpx accepts tasks/* from a 2025-06-18 client and subscriptions/listen from a legacy one — and it will not put structuredContent in front of a 2025-03-26 client, will not stamp resultType on a reply to anything but 2026-07-28, and will never send elicitation/create to a client that did not declare elicitation.

The rule stops at removal (§2.1): a method a revision removed is method-not-found to a peer on that revision.

Since the first version of this page, every sentence in it has been checked against a schema validator (internal/mcpspec, the five official schemas, strict mode) and the official conformance suite (spec/official-suite.md). Several were wrong, and the corrections are why this is increment 3.


1. The revisions

erahandshakeversion carriedmcpx servesmcpx speaks upstream
2024-11-05legacyinitializeonceyes, over stdio and Streamable HTTP; its HTTP+SSE server transport is not hostedwhen the server answers with it; HTTP+SSE as the last fallback
2025-03-26legacyinitializeonceyeswhen the server answers with it
2025-06-18legacyinitializeonceyeswhen the server answers with it
2025-11-25legacyinitializeonceyesoffered on every initialize
2026-07-28modernnone_meta, every requestyesprobed first

The split is the thing everything else follows from. A legacy implementation negotiates once and can send its peer a request at any time afterwards. A modern one has no handshake, restates its version and capabilities on every request, and — as a server — has no channel at all on which to send a request of its own. The matrix is unforgiving: modern against legacy fails, legacy against modern fails, only a dual-era implementation bridges them. mcpx is dual-era in both directions.

Upstream, mcpx probes modern first and remembers the answer: the 2026-07-28 versioning page makes era “a property of the server”, and prescribes that order for a dual-era client. The era is cached per server configuration, in memory and in <state>/upstream-eras.json, so the probe costs one round trip per configuration rather than one per start. It used to probe legacy first, on the argument that nearly every server is legacy; the cache removed the cost that argument was about. spec/era-probe.md has the order, the fallbacks and the cache.

Downstream, Oldest is 2024-11-05: what a request that declared nothing is spelled for, because every shape that revision defines is understood by everything newer. Guessing downward is recoverable; guessing upward hands a peer a frame it cannot parse. A headerless Streamable HTTP request, though, is judged as 2025-03-26 (Headerless): 2024-11-05 has no Streamable HTTP, so nothing arriving on /mcp can be a 2024-11-05 request that omitted the header, and the later transport pages say to assume 2025-03-26.

1.1 When revisions conflict: spec.precedence and spec.lenient (#307)

Most differences between revisions are settled by the revision the peer states. Where two revisions require incompatible behaviour and the peer’s version does not settle it, two settings decide (configuration.md):

  • spec.precedence — the revisions in the order their rules win, first wins. Default: every revision mcpx serves, newest first (2026-07-28,2025-11-25,2025-06-18,2025-03-26,2024-11-05). A partial list is completed with the rest, newest first. --mcp-spec <rev> / MCPX_MCP_SPEC (spec.first) moves one revision to the front.
  • spec.lenient — revisions mcpx does not hold strictly. Empty by default: every revision is strict.

An unknown revision in any of the three is refused with the list of valid ones. The daemon reports the effective policy under spec in GET /v1/protocol; mcpx settings shows each setting and where it came from. Code consults it through internal/spec (First, Strict, and the predicates AtOrAfter, Before, Only, Between).

Two behaviours consult it today:

behaviourdefaultchanged by
progress message relayed to a 2024-11-05 client (whose schema has no such field)sentstripped only when 2024-11-05 is first and strict: --mcp-spec 2024-11-05
a request from a 2026-07-28 upstream server (that revision has none; its stdio page says the client MUST NOT answer)droppedanswered when spec.lenient includes 2026-07-28

Each other conflict in spec/revision-conflicts.md carries a disposition there — peer-settled, precedence-governed, a recorded reading, or undisposed with its open question named. Only the precedence-governed ones are decided by these settings.

internal/mcpserver/revisions.go holds the floors, ceilings and removals as data, and GET /v1/protocol serves them. A matrix in a document and a matrix in code agree on the day they are written and never again, so the document defers to the endpoint.


2. mcpx as a server

What a host connected to mcpx gets, over mcpx serve (stdio) or the daemon’s /mcp (Streamable HTTP).

2.1 Methods

methoddefined inmcpx acceptsmcpx answers
initializelegacylegacy peersthe version asked for if mcpx speaks it, otherwise the latest legacy version it speaks — a result, not an error, as every legacy lifecycle page requires
server/discover2026-07-28alwayssupportedVersions (newest first), capabilities, instructions; serverInfo in _meta; ttlMs/cacheScope
pinglegacylegacy peers{}
tools/listallalwayspaginated (mcp.pageSize), opaque cursor, a deterministic order
tools/callallalways§2.3 and §3; an unknown tool is -32602
prompts/list, prompts/getallalwaysnot found is -32602; an upstream failure -32603
resources/list, resources/read, resources/templates/listallalwaysbinary contents as blob; not found is -32002 (legacy) or -32602 (2026), both with data.uri
resources/subscribe, resources/unsubscribelegacylegacy peers{}, always; subscribes upstream where the owning server can deliver, and otherwise publishes a warning event (§4.3)
subscriptions/listen2026-07-28any eraan acknowledgement, then only the notifications the filter asked for, each tagged with the subscription id
completion/completeallalwaysforwarded to the server that owns the ref, as /v1/complete does, with context.arguments; a 2024-11-05 upstream is asked without a capability (that revision had none); unknown ref -32602, upstream failure -32603
logging/setLevellegacylegacy peers{}; sets the level of upstream log messages relayed to this connection (§4.4)
tasks/get, tasks/list, tasks/result, tasks/cancel2025-11-25 coreany legacy peerthe core shapes; a task is visible only to the connection that started it
tasks/get, tasks/update, tasks/cancel2026-07-28 extensionmodern peers that declared io.modelcontextprotocol/tasks on the request; -32021 with data.requiredCapabilities (HTTP 400) otherwisethe extension’s shapes; tasks/list and tasks/result are -32601, as the extension says
skills/list, skills/getskills extension (SEP-2640)always, unless the server offers only contributed toolsmcpx’s own skills (plugin/opencode/skills, embedded at build), each file at skill://mcpx/<name>/<file> and also listed and readable as a resource; an unknown URI is -32602. Upstreams’ skills are not relayed: in-name-only.md
resources/directory/readskills extension, directoryReadwherever skills/list isthe direct children of a directory inside one of mcpx’s skills: files as resources/list lists them, subdirectories as inode/directory; paginated. A file or an unknown URI is -32602
notifications/cancelledallalwayscancels the request it names — its context, and so its upstream call — and withholds the reply

Two rows are the “accept liberally” rule doing visible work: subscriptions/listen from a legacy client and core tasks/* from any legacy client. mcpx answers both. It declares neither outside the revision that defines it, because a declaration is a promise about the revision in force.

The rule stops at removal. Offering a client a method its revision never had withholds nothing. Answering one its revision removed is different: the removal is the specification naming a replacement, and 2026-07-28 requires method-not-found — “If the server does not implement the requested RPC method, it MUST respond with 404 Not Found and a JSON-RPC error with code -32601”. The 404 is load-bearing rather than decorative: it is how a dual-era client tells a modern server from a legacy endpoint that simply is not there. So initialize, ping, logging/setLevel, resources/subscribe and resources/unsubscribe are -32601 to a 2026-07-28 peer and unchanged for every older one. internal/mcpserver/revisions.go holds the list as removedIn, keyed to the same feature ceilings the rest of the matrix uses. mcpx answered all of them to everyone until the official suite scored it (#250).

Tasks, per era. 2025-11-25 core tasks are client-directed: a task field on tools/call, {task} back at once, tasks/result blocks. mcpx honours the field from any legacy client. The 2026-07-28 extension is server-directed and forbids the other way: the task field is ignored, and a client that declared io.modelcontextprotocol/tasks on the request gets a CreateTaskResult (resultType: "task", ttlMs, pollIntervalMs). Which calls become tasks follows the tool’s execution.taskSupport: a task-supporting tool’s call is a task once it has run 250 ms, one that declared nothing only after protoMessages.taskAfter, and a required one is -32021 to a client without the extension. tasks/get inlines the result. Questions a task-supporting call asks at once go inline as input_required on the original request, before any task exists; ones it asks later park the task in input_required with inputRequests, answered through tasks/update. A legacy task shows input_required while one of its questions is out, and the question carries the related-task _meta, as does the tasks/result response. A legacy task-augmented call to a tool that declares no execution.taskSupport is refused -32601, as 2025-11-25 asks: of mcpx’s own tools only mcpx_call and mcpx_exec declare "optional" (to 2025-11-25 clients only; 2026-07-28’s Tool has no execution). A 2026-07-28 client that names tasks in a subscriptions/listen’s taskIds gets the ones it can see back in the acknowledgement, then notifications/tasks on that stream – the task’s current state at once, then every change, each the DetailedTask tasks/get would answer. spec/messages.md has the rest.

Unknown tools are a protocol error. tools/call naming a tool mcpx does not have is -32602 (“no tool named …”), as every revision’s tools page lists it. It used to be an isError result, which told the model to retry a call that could never work (#284).

2.2 Capabilities mcpx declares

capability2024-11-052025-03-262025-06-182025-11-252026-07-28
tools.listChangedtrue in pass-through mode when the connection can push, else falsesamesamesamesame
resources.subscribe, .listChangedwhen the connection can pushsamesamesamesame
prompts.listChangedwhen the connection can pushsamesamesamesame
completions— (the revision has no such capability; the method is answered anyway)✓✓✓✓
logging✓✓✓✓— (the level travels in each request’s _meta)
tasks (core)———✓—
extensions["io.modelcontextprotocol/tasks"]————✓
extensions["io.modelcontextprotocol/skills"]————{"directoryRead": true}
  • tools.listChanged is false unless mcpx passes tools through. mcpx’s own tool list is fixed when its server is built. It used to be declared and fed by upstream tool changes, which are not changes to this list: a client re-listed on every one and got the same tools back. In pass-through mode (--passthrough) the list is the upstreams’, so an upstream’s tools/list_changed is relayed – unsolicited to a legacy client, and on a subscriptions/listen that asked for toolsListChanged to a modern one.
  • logging is declared to legacy clients. The capability means “this server sends log messages”: mcpx relays its upstreams’ notifications/message to a client that set a level, during that client’s calls (§4.4, #212). 2026-07-28 has no such capability.
  • “Can push” is a property of the connection, not of the server. A stdio connection can. A legacy HTTP session can, through its GET stream (which used to be missing, so both were declared and never delivered). A legacy POST outside a session cannot, and gets neither. A 2026-07-28 client receives these only on a subscriptions/listen stream, which is its own request’s response, so any transport that can stream a response can deliver them.
  • Core tasks only to 2025-11-25, extensions only to 2026-07-28. Both were sent to both; 2025-11-25’s schema has no extensions field, and the extension’s SEP says a server must not keep advertising core tasks under a revision that has the extension.

2.3 Result shapes, and what is downgraded

Everything is built in the newest shape and spelled down once, at the edge, in downgrade(). An upstream’s tools/call result (through mcpx_call or a pass-through tool) and prompts/get result are carried as the upstream sent them – isError, every content block, structuredContent and _meta – with resource URIs rewritten to ones /mcp can read; listed tools, resources, templates and prompts keep their optional fields. Building several shapes and choosing between them is how the shapes drift apart.

carrieddefined fromto an older client
structuredContent2025-06-18removed, and rendered into the content array as text — the data survives, in a vocabulary the client has
resource_link block2025-06-18becomes an embedded resource with a text/plain label naming the URI, which keeps the URI machine-readable where text would not
audio block2025-03-26a text block describing it, for 2024-11-05
_meta on listed tools, resources, templates, prompts and read contents; annotations.lastModified2025-06-18stripped
tool annotations, the completions capability2025-03-26stripped for 2024-11-05
title2025-06-18stripped
icons2025-11-25stripped
resultType2026-07-28removed — sending it says mcpx is speaking a revision it is not
ttlMs, cacheScope, serverInfo in _meta2026-07-28removed; serverInfo travels in the initialize result instead
inputRequests / requestState2026-07-28never sent; a legacy client is asked on the wire instead
elicitationId2025-11-25never sent to 2026-07-28 (it used to leak through inputRequests); minted for a 2025-11-25 url-mode question an upstream sent without one

A revision that defines all of them pays nothing: the function returns the result untouched. An older one gets a copy, never an edit, because a task’s stored result is handed out more than once and rewriting it in place would downgrade it permanently for whoever reads it next.

The 2026-07-28 envelope. Every modern result carries resultType (complete unless it is input_required or task) and _meta["io.modelcontextprotocol/serverInfo"]. The cacheable ones carry the REQUIRED ttlMs and cacheScope: public for server/discover, tools/list, prompts/list and resources/templates/list, which follow configuration rather than the caller; private for resources/list, which includes the caller’s own session artifacts, and resources/read. The ages are protoMessages.listMaxAge (1m) and protoMessages.readMaxAge (0s). An input_required result carries no hints — it is an interim answer.

The strict schema sweep (internal/mcpserver/schema_sweep_test.go) validates every frame mcpx sends against each revision’s schema.json and rejects a property the revision never defined. It is how the discover shape, the missing cache hints, the listen acknowledgement’s shape, extensions sent to 2025-11-25 and a uri where resources/templates/list needed uriTemplate (in every revision) were found. spec/messages.md has the per-revision list.

2.4 Transport and identity

The rules are in spec/transport.md; what matters for the rest of this page:

  • stdio runs requests concurrently, each under its own context, so a notifications/cancelled can be read while the request it names is running, and cancelling it reaches the upstream call. Notifications and initialize stay in line. At EOF, requests in flight get transport.stdioDrain to answer.
  • Streamable HTTP, legacy: only initialize mints Mcp-Session-Id; an unknown or terminated session is 404; DELETE ends one; a session has a GET stream for what mcpx pushes; within a session a dropped connection is not a cancellation, and notifications/cancelled is.
  • Streamable HTTP, 2026-07-28: MCP-Protocol-Version, Mcp-Method and Mcp-Name are required and must agree with the body (-32020, 400); a missing _meta is -32602 (400); the HTTP status is a function of the error code; closing the stream is cancellation; no sessions, none minted, none echoed.
  • Origin is validated on every request: loopback, the daemon’s own address and transport.allowedOrigins; anything else is 403.
  • Who a request is from is decided per connection (spec/identity.md): _meta["dev.mcpx/session"] if the client names one; otherwise the process on stdio, the Mcp-Session-Id on a legacy HTTP session, and nothing — its own per-request scope — for a 2026-07-28 request. Every /mcp client used to share the daemon’s pid and cwd, and so one stateful instance and one set of artifacts.

MCP over ssh

An MCP host that can only spawn a process gets a remote daemon the same way the CLI does: with ssh as the transport and no streamable-HTTP endpoint to configure.

{ "mcpServers": { "mcpx": { "command": "ssh", "args": ["buildbox", "mcpx", "serve", "--transport", "stdio"] } } }

mcpx serve on the remote host is a stdio shim over the daemon there, so the tool calls, elicitations and artifact bytes all belong to that daemon. No port is opened and the only credential is the ssh one. For a client that sets a daemon endpoint instead, ssh://buildbox is the shorter form and the client starts ssh itself — see remote.md.


2.5 Pass-through: upstreams under their own names

By default mcpx is a gateway: a small tool set (mcpx_call, mcpx_exec, discovery), prompts as <namespace>_<prompt>, resources as mcpx://<namespace>/<uri>. mcp.passthrough (--passthrough <server> on mcpx serve and mcpx daemon, MCPX_MCP_PASSTHROUGH) names a configured server – or several, comma-separated, --passthrough demo,tasks – whose surface is offered as itself instead:

  • tools/list carries its tools under their own names, ahead of the gateway’s; tools/call on one of them is forwarded and its result returned verbatim — images, audio, embedded resources, structuredContent and isError intact; a JSON-RPC error the upstream answers with is relayed as that error, not turned into an isError result. Its execution.taskSupport is carried on tools/list and decides whether a call runs as a task (“Tasks, per era”, §2.1). Its questions reach the calling client exactly as mcpx_call’s do (§3), with the upstream’s own inputRequests keys where it asked in a 2026-07-28 result.
  • prompts/list names its prompts without the prefix; prompts/get returns the upstream’s result verbatim.
  • resources/list, resources/templates/list and resources/read use its own URIs, as do subscriptions and completion/complete refs. An mcpx:// URI still reaches every other server and mcpx’s artifacts.
  • Its declaration, not mcpx’s. A 2026-07-28 client’s clientCapabilities go to the upstream with the call, narrowed from mcpx’s own: mcpx never declares to the upstream a capability its client did not (a tool that needs sampling is refused rather than run on mcpx’s say-so). An upstream JSON-RPC error — -32021 with its requiredCapabilities, -32602 for a name it does not know — is returned as that error, HTTP 400 included, not as a tool result with isError.
  • Names it does not list. A tools/call for a name that is neither the upstream’s listed tool nor a gateway tool is forwarded; the upstream says whether it exists. A server may answer to diagnostic or hidden tools it does not list.
  • Roots. A roots/list the upstream asks in an input_required round is put to a client that declared roots, as an elicitation is; for one that did not, mcpx answers with its own configured roots.
  • Several upstreams are merged into one surface. A tool or prompt name two of them offer is refused – tools/list or prompts/list fails with an error naming both – rather than handed to either, since the loser would be unreachable under the name the client was shown. A bare resource URI goes to the first upstream, in the order given, that lists it as a resource or offers a template it falls under; one none of them lists goes to the first.
  • Collisions with the gateway: the upstream wins. A gateway tool whose name the upstream also uses is neither listed nor callable over MCP (it stays on the CLI and /v1). The upstream is the server in this mode, and a client must be able to call every name tools/list shows and get that tool.

Every other server stays reachable through the gateway tools. Not carried over: a pass-through tool’s outputSchema, title and annotations (the daemon’s catalogue keeps name, description, input schema and execution only). The upstream’s progress and log messages during a call are relayed (§4.4). scripts/conformance.sh runs the official suite’s own fixture server this way, merged with internal/testsupport/taskmcp, mcpx’s fixture for the suite’s tasks-extension scenarios, which the suite’s server does not define.

3. A server asks a question

This is the part with two mechanisms for one thing, and the reason the rest of the design is shaped the way it is.

When mcpx proxies a call and the upstream server elicits or samples, the question becomes a row in the broker: an identity, a deadline, a routing decision (docs/elicitation.md). What the broker could not do alone is ask mcpx’s own client — the agent or editor that made the call, which is very often the only thing that knows the answer.

It does now, native-first and broker-backed (proto.native). The broker remains the store and the fallback; the client is asked first when it said it could answer.

3.1 Which client gets asked

the client declaredversionwhat happens
nothinganybroker only
elicitation2024-11-05, 2025-03-26broker only; those revisions have no elicitation
elicitation2025-06-18asked, form mode only, mode and elicitationId stripped
elicitation (any shape)2025-11-25, 2026-07-28asked in form mode; a url-mode question stays with the broker
elicitation.url2025-11-25, 2026-07-28asked in either mode
samplinganyasked for sampling/createMessage only

Declaring one is not declaring the other. A sampling-only client is never sent an elicitation, and vice versa. Anything mcpx may not send this client is left with the broker and its default audience, so nothing is lost by a client’s narrowness — it just does not get to answer. Two gates are coarser than they should be (#210): any elicitation declaration counts as form mode, though a 2025-11-25 client that declared only url has not offered forms; and sampling is not checked for sampling.tools or sampling.context.

Why a non-declaring 2026-07-28 client never gets -32021 for a tool that asks. 2026-07-28 basic/index says a server “MUST NOT rely on capabilities the client has not declared” and “if processing a request requires a capability the client did not include … MUST return” -32021 (MissingRequiredClientCapabilityError). The error is conditional on requiring the capability, and mcpx never does: a question it may not send the client goes to the broker, which is a durable queue answerable by any consumer (mcpx elicit, /v1, the plugin, a human) whether or not one is listening when it is asked – there is no state in which the broker has no audience, only one in which nobody has answered yet, which ends in the question’s own timeout. mcpx also cannot know in advance: whether an upstream tool elicits is decided by the upstream mid-call, so a pre-dispatch -32021 would be a guess. The official suite’s sep-2575-server-rejects-undeclared-capability assumes a fixture tool (test_missing_capability) that mcpx does not publish, and so does not test this path (#252 item 1). The extension’s own methods are different: tasks/get, tasks/update and tasks/cancel from a client that did not declare io.modelcontextprotocol/tasks answer -32021 with data.requiredCapabilities (HTTP 400), because there the capability is the whole request; so does a subscriptions/listen asking for task notifications without it.

The message is rewritten to name the originator: “github (via mcpx) asks: which repository?”. A client asked “are you sure?” with no idea who is asking cannot answer it.

3.2 Legacy: a request on the wire

The server sends elicitation/create while the client’s tools/call is still open, and waits.

Over stdio this is straightforward — the pipe is the session, it stays open for the life of the process, and mcpx’s own receive loop routes inbound frames by shape rather than by id. Requests run on their own goroutines, so the answer can arrive while the request that asked is blocked on it.

Over Streamable HTTP it is not, and this is the part worth recording. The POST is the only channel, and it is the thing waiting for the answer. So:

  1. mcpx issues Mcp-Session-Id on initialize.
  2. A request that has to ask turns its POST response into an event stream — lazily, so a request that asks nothing still gets a plain JSON object — and writes elicitation/create as an SSE frame.
  3. The client answers with a separate POST carrying the same session header, whose body is a JSON-RPC response. mcpx routes it to the request waiting on that session and answers 202.
  4. The original stream carries the final result and closes.

A request that asks nothing but is slow — a pass-through call waiting out pool.callTimeout on an upstream that went quiet — also becomes an event stream once it has been silent for transport.sseKeepAlive, carrying a comment each interval until the answer, when the client’s Accept offered text/event-stream. Before, such a POST got no bytes at all, not even headers, for up to two minutes, and a client or proxy with an idle timeout gave up on a call that was still running. The stream carries no event ids: mcpx does not offer SEP-1699 resumption, and an id would promise a replay it cannot make.

A legacy POST outside any session cannot be asked anything: its answer would arrive on a POST nothing can route back. Its questions stay with the broker.

That is the reason Conn exists: a subscription filter, a push function and a client’s declared capabilities used to live on the Server, which was correct while stdio was the only transport and wrong the moment one server answered several HTTP clients — those fields decide whose client gets asked a question.

Server ids are negative on the wire. JSON-RPC only requires uniqueness within a direction, but several clients key one table by id, and they would otherwise see mcpx’s request answer their own.

3.3 Modern: a result the client retries

A 2026-07-28 server has no connection to send a request on. It answers resultType: "input_required" with a map of inputRequests and an opaque requestState; the client answers each and sends the same request again with inputResponses and the state attached. Only tools/call, prompts/get and resources/read may do this.

inputResponses sent on a request that resumes nothing — a client that knows what will be asked and answers up front — are held until the upstream asks, and given to the questions whose keys they match. Keys that match nothing are ignored, as SEP-2322 asks of information a server does not recognise.

Which means the call has already returned by the time the answer exists. The upstream call therefore cannot be tied to the client’s request — if it were, there would be nothing left to resume. It runs as a daemon task (internal/tasks, the same store /v1/call’s task option uses, collectable from the same /v1/tasks/{id}), and questions are correlated to it.

requestState is signed and bound to the request. The specification says the client must treat it as opaque, and that it SHOULD be tamper-evident and bound to the originating method and parameters, an expiry and — where there is one — the principal. mcpx’s is base64(payload).HMAC-SHA256 under a key generated per daemon process and never persisted, carrying the call id, a binding (the method and a SHA-256 of the parameters without _meta, inputResponses and requestState) and an expiry (proto.stateTTL). A token that verifies but was issued for another request is refused with a message that says so — the difference between a confused client and somebody replaying matters to whoever reads the log.

It used to be bound to the connection: an Mcp-Session-Id, which 2026-07-28 does not have. mcpx minted one on server/discover to have something to bind to, a conformant client never sent it back, and every such client got “still asking for input after 8 rounds” within milliseconds. Binding to the request needs nothing from the transport. mcpx has no authenticated principal to add, so a state is replayable by anyone who holds it, on the same request, within its TTL.

3.4 Correlation: which call asked?

A question arrives on a connection, not on a call. The server sends elicitation/create down the same pipe it is answering tools/call on, and nothing in the frame says which call it belongs to.

What mcpx knows is that one pooled instance serves one scope key. So a question arriving on (server, key) belongs to whatever is calling on (server, key), and the pool now tells the handler which key the connection serves.

When two calls share a key — which a shared server allows — the attribution is genuinely ambiguous. mcpx makes none, and the question goes to the broker. Guessing would hand one client’s credential prompt to another client, and that is not a trade worth making for a convenience.

The rule is one comparison. The ask table holds the upstream requests a call owns; the pool counts every upstream request in flight on the key, whoever made it (/v1/call, exec, the CLI, the plugin). A call owns a question when all the table’s entries on the key are its own and their number equals the pool’s count — nobody else is on the connection. Until #229 the table alone decided, and an interruptible call sharing a key with somebody’s ordinary mcpx call was handed that caller’s question. One window remains: an entry is registered an instant before its request is counted, so a stranger’s question arriving in exactly that instant can still be misattributed.

A script (mcpx_exec) is one call that makes many. It runs as an interruptible call too (/v1/ask kind exec), and its run id is chosen and registered before the script starts. The generated client sends that id as X-Mcpx-Run on every /v1/call, so each upstream call the script makes joins the table as a member of the run. A question on a key whose in-flight calls all belong to one run is that run’s — including calls the script makes in parallel. Two runs, or a run and anyone else, on one key: ambiguous, broker. When the run ends (finished, timed out, abandoned) its membership is removed everywhere, so a straggling upstream call killed with the script cannot pin a question to a call nobody polls; that question stays with the broker.

A script that asks more than proto.askRounds questions in one legacy request is abandoned like any other call — scripts that elicit in a loop hit it first. The script’s own exec.timeout (120s) is usually shorter than proto.askTimeout and a person answering eats into it; set timeoutSec on the tool call for a script that expects to ask.

3.5 Bounds

  • proto.askTimeout (10m) — how long one client request may be held. A legacy client is blocked for all of it, so it has to sit inside whatever that client’s own timeout is. A modern one is not blocked at all.
  • proto.askRounds (8) — how many times one call may come back asking. A server that never stops asking is broken or adversarial. For a 2026-07-28 client each round is a new request, so the count rides in the signed requestState and holds across the retries; it used to restart at zero on every one (#201).
  • proto.stateTTL (30m) — how long a requestState may be resumed with.
  • proto.askTTL (15m) — two clocks for a parked /v1/call: the waiting deadline (a call still waiting for an answer after 15m is cancelled) and, once the call finishes, how long its result is kept, measured from when it finishes.
  • pool.callTimeout pauses while a question is pending on the instance (#229). It used to kill a call while a person was answering it, which made the three settings above promises the pool did not keep. The precedence of all five timeouts is written down in internal/pool/budget.go. A caller may request a shorter value for a single call (--pool-call-timeout, carried as a call setting): the daemon clamps the request to its own configured value, so a call may lower the bound but never raise it (#1864). Against exec.timeout: that bounds a whole script run, this bounds one upstream call inside it – the run’s total work is capped by exec.timeout, each call within it by the (possibly per-call) pool.callTimeout, and whichever is tighter for a given call is the one that fires. The pool’s execution watch keeps the configured value; the per-call request bounds the call it was made on only.
  • The question’s own deadline is elicit.ttl in defaults.json (120s; a built-in, not a user setting). Expiry answers cancel, never decline: dismissed without choosing is not the same as refused.

A call the client stops waiting for — it cancelled, or its connection went — keeps running as a task, and its questions keep their deadlines. The result can still be collected from /v1/tasks/{id}; nothing interrupts the upstream call. That is deliberate for work somebody may come back for, and it is also why cancellation does not reach an interruptible call (§6).


4. mcpx as a client

What mcpx sends upstream, and what it will accept from a server.

eramodern probed first (server/discover), legacy on fallback; the answer cached per server configuration (spec/era-probe.md)
per-server overrideprotocol: prefer-discover | prefer-initialize | force-discover | force-initialize | follow, over upstream.protocol (default prefer-discover); the old names modern, legacy, force-modern, force-legacy and others are accepted as aliases (spec/era-probe.md); follow adds a legacy session for legacy callers (spec/era-probe.md)
legacy version2025-11-25 offered; 2024-11-05 .. 2025-11-25 accepted; anything else and mcpx disconnects
modern version2026-07-28
declared to serverselicitation.form and roots always; elicitation.url and sampling when a handler is installed — which in the daemon is always (#210)
transportsstdio, Streamable HTTP, and HTTP+SSE (2024-11-05) as the last fallback of the HTTP probe, remembered in the era record
stdio shutdownclose stdin, wait stdioShutdown.stdinGrace (2s, a built-in default) for the server to exit, then SIGTERM the process group, wait stdioShutdown.termGrace (3s), then SIGKILL — every revision’s stdio SHOULD (#203 LV-39; it used to SIGTERM at once)
headerslegacy: MCP-Protocol-Version is the negotiated version, omitted on initialize and up to 2025-03-26; modern: Mcp-Method, Mcp-Name, Mcp-Param-* for x-mcp-header parameters, never a session id

The per-revision rules — headers, x-mcp-header, resultType, cancellation, pagination, subscriptions/listen, resumption, the GET stream — are in spec/client.md. The official suite’s client leg passes every non-auth scenario (spec/official-suite.md).

The probe used to be the other way round, and it did not matter which way, because mcpx read protocolVersions from server/discover where the schema says supportedVersions. mcpx could not connect to any real modern server. Its own server made the same mistake, which is why mcpx talking to mcpx passed every test.

4.1 Server-initiated requests mcpx answers

requestmcpx answersregardless of era
ping{}yes
roots/listthe daemon’s roots, before consulting any handler — and the daemon passes none, so the list is empty (#210)yes
elicitation/createthrough the broker, or {"action":"cancel"} when nothing can answer; a mode that was not declared for the revision in use is refusedyes
sampling/createMessagethrough the broker; a decline becomes an error, since the specification has no decline result for samplingyes
anything else-32601, but always answered — silence is indistinguishable from a hung serveryes

Always answered, unless the server withdraws the question first: a notifications/cancelled naming a request still being answered ends the handler’s context (so a question relayed to the host is withdrawn too) and no response is sent. The reason, if given, goes to the daemon log as a server.warning. A cancellation naming anything else is ignored.

A modern server asks the same three things by answering input_required, and mcpx resolves them through the identical function. One function is what stops the two eras answering the same question differently.

4.2 Notifications mcpx listens to

notifications/message, notifications/progress, notifications/cancelled, notifications/{tools,resources,prompts}/list_changed, notifications/resources/updated, notifications/elicitation/complete.

From a legacy server they arrive unsolicited (on the standalone GET stream, over HTTP). A 2026-07-28 server sends them only on a subscriptions/listen stream, so mcpx opens one per upstream connection, with a filter built from what the server declared and the URIs somebody subscribed to, and reopens it when it ends. Messages and progress go to /v1/events; a list change drops the cached schema. notifications/tasks/status (2025-11-25) is received and dropped — mcpx polls task state rather than tracking it. A request from a 2026-07-28 server is not answered: that revision has no server-to-client requests (a server asks through input_required results), and the stdio transport page says the client MUST NOT respond (#200). With 2026-07-28 in spec.lenient it is answered like a legacy server’s (§1.1).

4.4 Progress, log messages and trace context, relayed (#212)

During a tools/call that reaches an upstream, mcpx carries what the host asked for through to the server doing the work, and what that server sends back to the host:

  • Progress. A host’s _meta.progressToken is replaced upstream by a token of mcpx’s own (mcpx-<n>, unique on that upstream connection, which two hosts’ tokens are not), and each notifications/progress for it is sent to the host with the host’s token restored. Only progress for a call still in flight is passed on: a token nobody asked for, or one whose response is in, is dropped. A report that does not exceed the last one passed on is dropped, so the host never sees progress go backwards; once progress reaches total nothing more is sent; and reports for one token are passed on no more often than every 20 ms (mcpclient.ProgressMinInterval), except the final one. A message goes to every host by default; it is removed for a 2024-11-05 host only when 2024-11-05 is first in spec.precedence and strict (--mcp-spec 2024-11-05), since that revision’s schema has no such field (§1.1).
  • Log messages. A 2026-07-28 host’s _meta logLevel is sent upstream (or the daemon’s own level, if more verbose); a legacy host’s level is the one it set with logging/setLevel. Upstream notifications/message at or above that level are sent to the host. No level, no messages. A log message names no request, so one arriving while a host has several calls in flight on the same upstream connection goes to each of them.
  • Trace context. traceparent, tracestate and baggage in _meta are passed upstream unchanged.

Over Streamable HTTP these go on the call’s own response stream, which becomes text/event-stream only when something is relayed; over stdio, as notifications. Between mcpx serve and the daemon the call carries a relay object on /v1/call (answered as application/x-ndjson when anything was relayed, plain JSON otherwise) or /v1/ask (collected by the polls as notifications). mcpx call and mcpx exec send no relay and print only the result. Messages and progress still reach /v1/events as before.

Not yet: progress does not extend mcpx’s own call timeout (CODE-13), and prompts/get and resources/read relay nothing.

4.3 Resource subscriptions, end to end (#241, #247)

mcpx declared resources.subscribe to its own clients and never subscribed upstream, so notifications/resources/updated never flowed: a capability declared and not delivered. Now a client subscribing to mcpx://<namespace>/<uri> – by legacy resources/subscribe (stdio, or Streamable HTTP with the update on the GET stream) or by resourceSubscriptions on a 2026-07-28 subscriptions/listen – makes the daemon subscribe <uri> on the server that owns the namespace, with whichever mechanism that server’s era has.

  • The lifetime is a /v1/events stream. mcpx serve hears the daemon’s events over GET /v1/events?uri=mcpx://...; a stream naming a resource in mcpx:// form (or bare with server=) is a subscription to it, held until the stream closes. So a listen stream ending, a connection ending, a legacy unsubscribe (which replaces the connection’s stream), or mcpx serve crashing all release it without a separate call that could be missed, and a stream that reconnects to a restarted daemon subscribes again by reconnecting. The stream’s first event, resource.watching, says what was subscribed and why anything was not.
  • Counted per URI across every client. The upstream sees one connection, mcpx’s, and one resources/unsubscribe from it would end the updates for everyone. The first watcher subscribes; the last one unsubscribes (internal/pool/watch.go).
  • Instances. One holding a subscription is not idle and is not reaped. A newly started instance is given every subscription still wanted, and one that ends while its subscriptions are wanted – mcpx restart, a crash – is replaced at once, since nobody else would start it and the updates would stop without a word. A replacement that fails to start is the pool’s ordinary start failure; the next call to that server retries and resubscribes.
  • A server that does not declare resources.subscribe is never sent a subscription (that would be asking for an undeclared capability). mcpx keeps declaring resources.subscribe itself – it is one capability over many servers, and it does support the mechanism – and is honest per resource instead: the listen acknowledgement’s resourceSubscriptions lists only the URIs whose updates will arrive, which is what the acknowledgement exists to report. A legacy resources/subscribe for such a URI – or for one no configured server owns, like a bare test://x – succeeds and delivers nothing (#251). A subscription is a standing interest, not a lookup: the spec does not require the resource to exist or its updates to be deliverable, and the legacy revisions have no way to say “agreed, but nothing will come”. Refusing with -32602, as #247 first did, broke clients that subscribe before they know (the official suite’s server-resources-subscribe and -unsubscribe among them). The reason is not lost: the daemon publishes a server.log event at level warning (“no updates will be delivered for : ”), visible on /v1/events. Honesty stays where the protocol has a mechanism for it, the listen acknowledgement. Only agreed URIs’ updates are ever sent on a stream.
  • Absolute-path URIs. A listing drops a leading / when it namespaces (/abs/doc is listed as mcpx://demo/abs/doc). The daemon resolves the inner part against the server’s own resource list to recover /abs/doc, and events.Filter compares URIs without a leading /; before, the two forms never matched.

4.4 Generic requests

Client.Request and Pool.Request send any method. Before them the typed helpers were the only way to the wire, and anything outside that set had no path at all — which is why POST /v1/complete answered from cache with "upstream": false. Not because the server could not complete: because nothing could ask it.

Complete now forwards completion/complete to the server, which knows the values an argument may take where mcpx knows only the names it has cached. A server that never declared completions is not asked — a well-behaved one answers method-not-found and the rest answer something unpredictable, so the declaration is the only thing worth trusting — and the reply says "source": "cache" with the reason.


5. One HTTP server

The daemon served /v1 on a unix socket and a loopback port. mcpx serve --transport http was a separate process serving /mcp, /v1/tools/*, /v1/call/<ns>/<tool>, /health and /openapi.json. Two HTTP servers, two overlapping /v1 prefixes, and which one a caller reached decided which half of the API existed — with no way to tell from the outside.

Now:

wasis
serve --transport http → /mcpthe daemon mounts /mcp on its own listeners (proto.mcpPath, proto.serveMCP)
serve --transport http → /v1/tools/<tool>POST /v1/tools/{tool} in the ops table
serve --transport http → /v1/call/<ns>/<tool>POST /v1/call/{server}/{tool} in the ops table
serve --transport http → /healthGET /v1/health, which already existed
serve --transport http → /openapi.jsonGET /v1/openapi.json, which already existed, and which now describes the per-tool paths the second server used to publish alone
serve --transport stdiounchanged

serve --transport http is removed, and asking for it fails with the address to use instead. A command that fails without saying where the thing went costs somebody an afternoon.

The daemon package cannot import the CLI — the dependency runs one way, and inverting it to put the two servers together would be a worse cure than the disease. So the CLI builds the handler and the daemon mounts it: Server.MCP, Server.MCPPath, Server.MCPTool. Three fields.

stdio stays, and stays a thin shim. It is inherent rather than a choice: an MCP host starts its servers by spawning a process and talking down the pipe, so something has to be spawnable. Everything else the daemon already had — a socket, a port, a lifetime, pooled servers kept warm between calls — and giving MCP a second copy of all of it bought nothing.

To put MCP on a network: mcpx daemon --port 8899 --address 0.0.0.0, then http://host:8899/mcp. The warning about an unauthenticated API binding beyond loopback applies to /mcp exactly as it applies to /v1, and for the same reason: whatever can route to the port can run tools as you.

Why not an extra listener

An --mcp-addr on the daemon was the obvious alternative, and it is one address away from what --port already does. The reason not to is that the two surfaces are not separable: /mcp calls /v1 for everything, the MCP tools are generated from the /v1 ops table, and a deployment that exposed one without the other would be exposing half a program. One listener, one access-control decision.


6. What is still missing

Named, because a gap nobody wrote down is a gap somebody rediscovers.

  • Cancelling an interruptible call does not stop it. On a direct call, notifications/cancelled (stdio, legacy HTTP session) and a closed stream (2026-07-28 HTTP, sessionless legacy) cancel the upstream request. A call on the ask path runs as a daemon task and keeps running (§3.5).
  • Progress does not extend mcpx’s own call timeout. It is relayed to the host (§4.4), so a host’s timeout can reset on it; the daemon’s per-server callTimeout still cannot.
  • -32021 for a tool that asks is deliberately not raised (§3.1); whether the broker fallback discharges the MUST is #252 item 1.
  • Tasks. notifications/tasks/status is dropped; mcpx never task-augments a call upstream, so a tool with taskSupport: "required" cannot be called; an extension task never reaches input_required (#209).
  • Capabilities declared upstream that nothing answers: sampling and elicitation.url whenever a handler exists, roots with an empty list (#210).
  • Authorization. No OAuth client and no OAuth on /mcp (#253). The per-server auth block (bearer, basic, header, query, env) is applied to every connection (#240); type: oauth is refused with a message, since the flow is not implemented.
  • Validation. Tool inputs and outputs and elicitation answers are not checked against their JSON Schemas (#254); sampling results are relayed unreviewed (#256); elicitation storage is readable by any local user and never pruned (#255).
  • Smaller conformance gaps: an invalid cursor restarts at page one instead of -32602, mcpx:// URIs are built without percent-encoding, no rate limits, annotations shown as trusted, and the rest of #257.
  • url-mode elicitation raised by mcpx itself. The pass-through works; mcpx never starts one of its own.
  • One attribution window (§3.4): an entry is registered an instant before its request is counted.

2026-09-30T16:00:00-05:00

Quick start

The whole loop, in under one screen: check the machine, add one server, look, search, read the types, call a tool.


mcpx doctor                                   # runtime, config, daemon: what is missing and how to fix it
mcpx servers add fs -- npx -y @modelcontextprotocol/server-filesystem /tmp
mcpx ls                                       # namespaces, tool counts, state
mcpx search directory                         # find a tool by name or description
mcpx types fs                                 # TypeScript signatures for this one namespace
mcpx exec 'emit(await fs.list_directory({ path: "/tmp" }))'

That is the loop from AGENTS.md, and it is the whole product in miniature:

  1. ls before anything. Discovery is a separate, cheap step from use — run it whenever; it starts nothing.
  2. search, then types for one namespace only. Signatures are loaded on demand, never all of them: loading everything is the failure mode mcpx exists to prevent. mcpx types fs.list_directory loads a single tool, cheaper still. Tool names come from the server — if list_directory is not what types shows, types is right.
  3. exec (or run) does the work. Inside a snippet, every namespace is a bare identifier and results arrive unwrapped. A named, saved version of the same idea is a script: mcpx run <name> runs .mcpx/scripts/<name>.ts.

Every command above is real: verified against internal/cli/manual.go (Commands(), the list behind mcpx help) and the dispatch table in internal/cli/dispatch.go at the commit this page was written. The server example is the one mcpx servers --help itself uses; it needs npx (node). With only deno or bun, find a first server that needs nothing extra instead: mcpx registry search <words>, then mcpx registry add <name> --write.

Do not start with mcpx init: it writes a starter config whose servers are placeholders, so the first ls after it shows an error row. Add a real server first, as above.

Make it an agent tool

The quick start above is a person at a shell. The step that makes mcpx an agent tool is one of:

  • give your agent AGENTS.md — the loop, the script contract and the secrets rules, in the file agents already read; or
  • install the opencode plugin — see install, “just the plugin” — and turn on what you want it to do: the plugin ships with tools and instructions off, so a bare copy gives your agent neither callable tools nor prompt guidance. For tools, name the plugin in config with options (["./.opencode/plugin/mcpx-session.ts", { "tools": true }], or set MCPX_PLUGIN_TOOLS=1); for the usage paragraphs, instructions: true or MCPX_PLUGIN_INSTRUCTIONS=1. See plugins for the full option list.

Next

Now what — config, surfaces, the plugin, and what to do when something is wrong.

Release runbook

How docs/story.md stays true. README.md is a symlink to it, so this is the first thing anyone reads about mcpx.

The file has two halves and they have different rules.

The two halves

Above <!-- table-of-contents-marker --> is the story. One agent, one prompt, one script, with the exact bytes it saw. It is the whole interface in one read. It is closed by default: fix it, do not grow it.

Below the marker is the feature list. It is open by default: any commit that lands on main may edit an entry or add one. It grows forever and that is fine, because nobody reads past the part that answers their question.

Rules for the story

Change it only to:

  • correct something that is now false — a flag renamed, output changed, a measured number that has moved;
  • replace a quoted block whose source changed. Every fenced block in the story is real output or a real file. Re-run the command, paste the result.

Do not add a new capability to the story. A new capability gets an entry below the marker. The story stays one prompt long.

Extending the story needs explicit sign-off from the repository owner. If a change would make the walk longer or add a second scenario, open it as a question first.

When a quoted block changes, verify it rather than editing by hand:

mcpx ls
mcpx search screenshot click
mcpx types chrome_devtools | head -20
mcpx scripts
mcpx run shot-flow

The instruction file quoted in full is AGENTS.md. If that file changes, the quote and the line-by-line breakdown after it both change.

Rules for the feature list

Ordering is meaning. Roughly:

  1. Core — what mcpx is for. Zero-context discovery, pools, the client. These change rarely and are read first.
  2. Standard — shipped, supporting. Scripts, config, ops, packaging.
  3. Proposed — wanted, not built. Everything under the ## Proposed rule.

New shipped work goes after the last standard entry unless it is genuinely core. New wants go under ## Proposed. Within a release, append in merge order.

Entry format

A small addition can be a heading and two links:

### [feat(a11y): support for Teletypewriter](https://github.com/developing-today/code/blob/main/pkgs/mcpx/docs/tty.md) #123

2026-10-02T11:30:00-05:00

Anything larger opens with a key block:

## Session-isolated process pools

​`
created:      2026-09-25T18:30:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
id:           session-isolated-process-pools
since:        pre-release
status:       core
tags:         area:pools, area:concurrency
description:  one sentence, present tense, what it does
​`

Prose, examples, the reasoning that is not obvious from the code.

2026-09-27T05:05:00-05:00

Required keys: created, last-updated, id, since, status, description.

Optional: owner, creator, tags, dek, note, editors-note, links.

increment is retired (removed in the #1865 migration). It counted commits inexactly; changed below counts recorded changes of meaning, exactly.

History metadata: id, since, changed

Every section carries a stable ID, frozen from the heading’s slug the day the metadata landed, so links never break when a heading is reworded:

  • id — the section’s ID, first key in the block.
  • since — the first release containing the section: a version (0.2.0), or unreleased for work not yet released, or pre-release for everything that predates the first release (the migration sentinel; no release has shipped).
  • changed — the section’s recorded history, one list item per change:
changed:

- 0.2.0 (2026-11-02): idle exit is now a setting

Headings without a key block use the compact comment form, directly under the heading:

<!-- id: pools-lifetime since: 0.2.0 -->

A change is recorded per PR as one fragment file, docs/changes/<slug>.md:

sections: pools, pools-lifetime
kind: Changed
summary: idle exit is now a setting

kind is a Keep a Changelog kind (Added, Changed, Deprecated, Removed, Fixed, Security). At release time stamp folds fragments into the named sections’ changed lists and deletes them. An edit that changes no meaning (typos, reflow, link repair) needs no fragment: mark the PR with the docs:trivial label or a Docs-Trivial: yes trailer.

status also accepts the closed-proposal values shipped, implemented and rejected (used by proposals.md); a status that needs more words becomes a vocabulary status plus a free-text note key.

The tool is tools/docs-history (one Go program, render / stamp / check on a single parser; not part of the shipped binary). render emits the since marker and one closed-by-default <details> history per heading whose subtree has history — its own entries in full, then one line per descendant change grouped by release — and with --agent the agent-twin form (no disclosures, a since/status line and a history link per section). It also speaks the mdBook external-preprocessor protocol on stdin/stdout.

tags are Datadog-style: bare tags (area:pools) and key-value labels (platform:nix, cost:context). Reuse existing keys before inventing one — current keys are area, platform, cost.

status is one word:

statusmeaning
coremcpx is not mcpx without it
standardshipped and supported
candidateshipped, may still change shape
alpha / betashipped behind a flag or unstable
proposedwanted, not built. Say so in the prose too
partialsome of it works; the entry says which part
deprecatedworks, will be removed. Link the replacement
brokenknown broken, not yet removed
retiredremoved. Struck through, or moved to docs/archive.md
shipped / implementedclosed proposal: built (proposals, document-level blocks)
rejectedclosed proposal: decided against

Every top-level entry ends with its last-updated timestamp on its own line, RFC 3339 with offset. It duplicates the key block on purpose: the key block is for machines, the trailing line is for whoever is scrolling.

Removing things

Never delete an entry. Either strike the heading through:

## ~~Old thing~~

and set status: retired with a note pointing at the replacement, or move the whole entry to docs/archive.md and leave a one-line stub linking to it.

Heavy refactors move the old text to the archive too. The archive is append-only and has no ordering rules.

Per-commit

Any commit that lands on main and changes behaviour:

  1. Edit or add the entry below the marker.
  2. Set last-updated, update the trailing timestamp, and add a change fragment naming the section IDs you changed (docs/changes/<slug>.md), or mark the PR trivial (docs:trivial label / Docs-Trivial: yes trailer). CI runs the docs-history check on PRs touching docs/**.
  3. If it changed something the story quotes, re-run the command and paste the real output.
  4. If it changed AGENTS.md, update the quote and the breakdown.

A commit that only changes behaviour covered by an existing entry does not need a new one; it needs that entry updated.

Per-release

  1. Run the docs-history check (see Checks): every entry touched since the last release must carry a change fragment or a trivial marker, and every since must be valid.
  2. Re-run the story’s commands and diff against what is quoted. This is the real check — the story is the only part that can silently rot.
  3. Move anything now retired into docs/archive.md.
  4. Promote proposed entries that shipped: change status, move them up into the shipped run, set created to when the work landed rather than when it was first wanted.
  5. Run stamp with the release version and date (go run ./tools/docs-history stamp --version X.Y.Z --date YYYY-MM-DD docs/*.md): it replaces since: unreleased, folds the fragments into changed lists and deletes them. Commit the stamped docs, then tag that commit — the tagged tree is the stamped tree.
  6. Re-read the first screen. If the first thing a newcomer sees is no longer the most important thing, reorder.

Checks

cd pkgs/mcpx
gofmt -l .
go vet ./...
go test ./... -count=1
MCPX=./bin/mcpx scripts/stress.sh      # concurrency, leaks, orphans
MCPX=./bin/mcpx scripts/bench.sh       # latency, against lootbox
(cd ../.. && nix build .#mcpx)         # unit suites in the sandbox

The docs-history check (duplicate IDs, bad since values, fragments naming unknown sections, and — with --diff — prose changes with no fragment and no trivial marker):

go run ./tools/docs-history check docs/story.md docs/proposals.md
git diff origin/main...HEAD -- docs/ > /tmp/docs.diff
go run ./tools/docs-history check --diff /tmp/docs.diff docs/story.md

README.md must stay a symlink to docs/story.md:

test -L README.md && readlink README.md   # -> docs/story.md

Reaching a daemon on another machine

The daemon speaks HTTP on a unix socket, and every client surface — the CLI, scripts, the TUI, MCP — goes through the same client. Which means a daemon on another machine is a transport question, not a feature: point daemon.endpoint at it and everything works there.

Three routes exist, in order of how much setup they need.

1. ssh:// — nothing to set up

export MCPX_DAEMON_ENDPOINT=ssh://buildbox
mcpx ls
mcpx tui

The client runs your own ssh binary (ssh -T -o BatchMode=yes buildbox mcpx stdio-proxy) and speaks the daemon’s HTTP API through it. Your ssh agent, ssh_config, known_hosts, jump hosts and Match rules are the access control, because it is literally your ssh doing the connecting. Nothing listens on the remote host beyond the daemon’s own unix socket, and no mcpx credential exists to leak.

Details worth knowing:

  • The remote socket path is resolved by the remote process, from mcpx’s own paths and configuration file there. The client makes no assumption about the remote filesystem layout.
  • ?bin=/path/to/mcpx names a remote mcpx that is not on PATH: ssh://buildbox?bin=/opt/mcpx/bin/mcpx.
  • ?sock=/run/user/1000/mcpx/daemon.sock picks a specific remote daemon, for a host running more than one.
  • user@ and :port work as in any ssh target: ssh://drew@buildbox:2222.
  • BatchMode=yes is set, so ssh never stops to ask for a password or a passphrase: an HTTP request has no terminal to answer on. Load the key in your agent, or use a key with no passphrase.
  • A remote endpoint is never auto-started. If nothing answers, the error names the host and the exact remote command that was tried; start the daemon there with ssh buildbox mcpx daemon (or mcpx daemon --detached).
  • Server-sent events (/v1/events, mcpx log --follow) stream over the same tunnel; there is no separate event path.

mcpx stdio-proxy is the remote half and is a normal command: it dials the local daemon socket and copies stdin/stdout to it. Running it by hand is the quickest way to check a host:

ssh buildbox mcpx stdio-proxy --sock /run/user/1000/mcpx/daemon.sock

2. ssh -L — forward the socket itself

For tools that cannot run ssh themselves, or when you want one tunnel for many shells, forward the unix socket as a local socket:

ssh -N -L /tmp/buildbox-mcpx.sock:/home/you/.local/state/mcpx/daemon.sock buildbox &
export MCPX_DAEMON_ENDPOINT=unix:///tmp/buildbox-mcpx.sock
mcpx status

The forward is a plain byte pipe, so it is the same thing the ssh:// transport does, just started by you instead of by the client. The remote path must be the socket the daemon there is actually using — mcpx status --json on the remote host prints it (socket).

ssh -L also works in the other direction of convenience: -L 127.0.0.1:8065:127.0.0.1:8065 forwards the daemon’s loopback TCP port, for a remote daemon started with daemon.address widened or a container that serves TCP. Prefer the socket: it is the same filesystem permission check as being there.

3. ssh -R — a daemon behind NAT

The two routes above dial out from the client, which fails when the daemon’s host cannot be reached at all. Reverse the direction instead: the daemon’s host connects out to a host that is reachable, and publishes the socket there.

On the daemon’s host (behind NAT), forward its socket to a socket on the public host:

ssh -N -R /tmp/team-mcpx.sock:/home/you/.local/state/mcpx/daemon.sock public-host

Then, anywhere that can reach public-host:

ssh -N -L /tmp/mcpx.sock:/tmp/team-mcpx.sock public-host &
MCPX_DAEMON_ENDPOINT=unix:///tmp/mcpx.sock mcpx ls

Or, with no local forward at all, use ssh:// from the middle host, since the socket is already there:

ssh public-host MCPX_DAEMON_ENDPOINT=unix:///tmp/team-mcpx.sock mcpx ls

ssh -R needs AllowStreamLocalForwarding (the default) on the server, and StreamLocalBindUnlink=yes in the forwarding client’s ssh_config to replace a stale socket after a dropped connection.

4. mcpx serve over ssh — MCP to a remote daemon

An MCP host that cannot set an endpoint can spawn ssh instead of mcpx: see the MCP-over-ssh form in protocol.md. The short version:

{ "mcpServers": { "mcpx": { "command": "ssh", "args": ["buildbox", "mcpx", "serve", "--transport", "stdio"] } } }

That runs mcpx serve on the remote host, so the daemon there is the one answering — again with no port and no credential beyond ssh.

What a remote endpoint is not

  • It is not a second authentication layer. The daemon API is unauthenticated; who may reach it is who may reach the host and the socket. ssh:// inherits exactly the ssh access you already grant.
  • It is not a shared filesystem. Scripts and artifacts sent to a remote daemon travel as bytes; a caller’s {path} is uploaded, not made visible to the daemon, because an ssh tunnel never proves shared-disk access. See exec.md.
  • It is never started from here. Local autostart does not apply, by design: answering from a local daemon when the remote one is unreachable would be worse than failing.

Research and incidental findings

created:      2026-11-01T01:30:00-05:00
last-updated: 2026-11-01T01:30:00-05:00
increment:    1
status:       draft
tags:         area:docs, area:compendium, topic:research
description:  what was learned while building mcpx, and while doing anything
              else in this repository, collected by topic and tagged with its
              provenance so it is not lost in session transcripts. First slice
              of #1871: the section outline, the seed findings that already
              exist in the repository, and the harvesting process.

This chapter is the compendium’s research chapter (#1871). It holds research and incidental findings — things learned while building mcpx, and while doing anything else in this repository — filed by topic so they survive the sessions that produced them. Findings that live inside feature entries stay there as the source of truth; this chapter excerpts them with a citation (the answer to open question 5 on #1871, applied: excerpt, never copy-and-fork).

Every finding here carries a provenance tag, per the quality bar below:

  • measured — a number or behaviour produced by running something, with the environment named in the source.
  • read-from-source — read in code, docs, or a specification in this repository.
  • reported — someone else’s claim, cited: an external article, another project’s documentation, or a statement in an issue. Reported findings are not verified here and must not read like measurements.

Sections with no seed yet are outline only: the heading exists so the next harvest has somewhere to put things, and says so rather than padding.

AI and LLMs

  • The cost model of zero-context discovery. Listing servers costs ~171 tokens, a search ~61, and types 675–4,402 per namespace — the numbers behind progressive discovery: a client should not pay for every tool definition up front. (measured — docs/story.md, “Zero-context discovery”)
  • Switch to progressive discovery at 1–5% of the context window, and do not mutate the tools array mid-conversation: it invalidates the provider’s prompt cache. (reported — modelcontextprotocol.io, MCP client best practices, as cited on #1871)
  • Models write code better than they call tools, Cloudflare argues, because tool-call tokens are synthetic training data while code is what models were trained on. This is the argument behind Code Mode. (reported — blog.cloudflare.com/code-mode, as cited on #1871)

Harnesses

  • What is opencode’s and what is general. docs/opencode-plugin.md separates the opencode plugin surface (v1 vs v2) from the parts that apply to any harness; OPENCODE-V2.md corrects the record where the first draft blurred them — opencode v1 ships code mode behind OPENCODE_EXPERIMENTAL_CODE_MODE. (read-from-source — docs/opencode-plugin.md, OPENCODE-V2.md)
  • The harness knows what the agent does not. Session state, statistics, and affordances live in the harness; an agent that assumes them instead of asking gets them wrong. The opencode session statistics are the worked example. (read-from-source — docs/story.md, “The harness knows what the agent does not” and “Statistics from opencode”)
  • AGENTS.md is the cross-harness instruction file: the same file is read by the harnesses this repository is used from, which is why fleet rules live there and not in one tool’s config. (read-from-source — AGENTS.md at the repository root and in pkgs/mcpx)
  • A harness-free core is a design constraint, not an accident (#48): codex, claude code, pi, and manus must all be able to drive the same core. (read-from-source — docs/story.md; issue #48)

Plugins

  • A plugin can connect without the binary. The connection ladder and GET /v1/resolve let the opencode plugin find or start a daemon instead of assuming one is installed (#44, fixed in #53). (read-from-source — docs/plugin-discovery.md)
  • The plugin logs over the socket (#36), so plugin diagnostics land where the daemon’s do instead of in a file nobody opens. (read-from-source — docs/plugin-discovery.md; issue #36)
  • Plugin install is a README, not a package step. (read-from-source — plugin/opencode/README.md)

Token providers

The outline assumes both readings of the term (open question 1 on #1871): model providers, and the credential/OAuth providers that hand out tokens.

  • Model providers are maintained as patches. This repository patches opencode for provider behaviour (Copilot business support, Copilot compaction, OpenAI response-id caching) and keeps skills for maintaining those patches across opencode updates. (read-from-source — #1871 seed list. Harvest note, 2026-11-01: the three patch files the issue names are no longer at the repository root on main; the patches’ current home needs naming here by whoever next touches the opencode patch set.)
  • **Credential placeholders keep secrets out of config files and descriptions, but stdio servers receive them through the environment. Generated scripts inherit that environment unless variables are unset or the run uses -i; do not treat this as isolation from code you execute. (read-from-source — docs/story.md, “Credentials”; internal/runner/dispatch.go:693–705)

Cloudflare

  • Code Mode mechanics. The Worker Loader API runs generated code in isolates; fetch()/connect() throw inside it; bindings hide API keys from the generated code. Shared with #153 — the design treatment lives with the design, this is the finding. (read-from-source — docs/story.md; issue #153)

The TypeScript ecosystem

  • Node runs TypeScript by stripping types, and only erasable syntax survives. Node v24.20.0 type stripping rejects a file with an enum; constructor parameter properties failed too, which is why the generated client uses plain fields. (read-from-source — ASSESSMENT.md bug 3; docs/story.md, “Runtime portability”)
  • Runtime detection order is deno, then bun, then node (script.runtime, --runtime, MCPX_SCRIPT_RUNTIME). Versions at the time of the seed: Deno 2.9.6, Bun 1.3.11. (read-from-source — docs/story.md, “Runtime portability”; the runtime comparison itself belongs to #101 and docs/runtimes.md)
  • --typecheck is deno check, Deno only: ~80 ms on repeat, ~400 ms for a new program (#79). (measured — as recorded on #1871; docs/story.md)
  • Constructing a TypeScript compiler per request was lootbox’s memory problem — the ts-morph lesson behind the current design. (read-from-source — ASSESSMENT.md)

SSE

  • Server-sent events, not WebSockets. SSE is plain HTTP, EventSource reconnects on its own, and WebSocket upgrades are mishandled by middleboxes — the reasoning behind mcpx’s remote transport. (read-from-source — docs/story.md, “Server-sent events, not WebSockets”)
  • Lossless reconnection is a protocol property, not a client hope: the stream carries what a reconnecting client needs to resume without a gap. (read-from-source — docs/story.md, “Lossless reconnection”)

Sockets and TCP

  • What each transport costs. Spawning mcpx status took 23.12 ms; a unix-socket keep-alive round trip 0.17 ms (135× cheaper); TCP loopback keep-alive 0.65 ms. The daemon-and-socket design is priced by these numbers. (measured — docs/story.md, “What each transport costs”)
  • macOS sun_path is 104 bytes. Deep state directories produced socket paths that failed with bind: invalid argument. (read-from-source — ASSESSMENT.md bug 4)
  • localhost is not 127.0.0.1. It resolved to IPv6 only while the compiled build bound 127.0.0.1, so the client dialled an address nothing listened on. (read-from-source — ASSESSMENT.md)

RPC and WebSockets

  • JSON-RPC framing needs no dependency — it is standard library, which is why it appears in the dependency audit as a deliberate non-dependency. (read-from-source — docs/dependencies.md)
  • Lootbox’s WebSocket. The predecessor system used a WebSocket RPC channel (lootbox.config.json: serverUrl: ws://127.0.0.1:9420/ws) — the contrast case for the SSE choice above. (read-from-source — #1871 seed list)

MCP

  • Two protocol eras. The protocol has a before and an after, and a client has to speak to both; “What 2026-07-28 actually asks of a client” is the checklist. (read-from-source — docs/story.md, “Two protocol eras”)
  • The revision matrix — every revision in both directions, plus what is still missing — lives in docs/protocol.md and is excerpted, not copied, here. (read-from-source — docs/protocol.md)
  • The sampling bug: it did not work, and nothing said so. Capabilities are declared in the handshake, but the handler was installed after it, and every test called the passthrough directly — so sampling was advertised, broken, and green in CI at the same time. (read-from-source — docs/story.md, “Sampling”)
  • The elicitation bug came first — the same shape, earlier. (read-from-source — docs/story.md, “Elicitation”; docs/elicitation.md)
  • mcpx doctor found a bug immediately — a diagnostics command earns its keep the first time it runs against a real installation. (read-from-source — docs/story.md, “mcpx doctor”)
  • Remote servers without a shim: Streamable HTTP makes mcp-remote unnecessary for remote servers. (read-from-source — docs/story.md, “Remote servers without a shim”)
  • Which registry. The official registry publishes an OpenAPI spec; HAPI is not open source and returns 404. Registry choice is a licensing and availability decision before it is a technical one. (read-from-source — docs/story.md, “Which registry”)

OpenAPI and REST

  • Specifications as tools: two things easy to get wrong. A relative server URL must resolve against the document’s location, and only read methods are exposed by default. (read-from-source — docs/story.md, “Specifications as tools”)
  • The specification described the wrong thing: the first spec described mcpx’s own ten tools, not the hundreds it fronts. (read-from-source — docs/story.md, “Specifications as tools”)
  • One ops table generates the routes, the MCP tools, and the OpenAPI document (internal/api/ops*.go), so the three cannot drift apart. (read-from-source — internal/api/)

OAuth

  • OAuth for remote servers is status: proposed, not built; the credential story that exists today is in “Credentials”. (read-from-source — docs/story.md, “OAuth for remote servers” and “Credentials”)

Command line

  • Command-line programs as servers: a CLI with a daemon behind it is a server with a command-line client, and should be designed like one. (read-from-source — docs/story.md, “Command-line programs as servers”)
  • Derived flag names surprise people: daemon.leaseTTL becomes --daemon-lease-ttl. (read-from-source — docs/cli.md:142–146)
  • Accepted-and-ignored flags are bugs, even when accepting them is deliberate-looking (#74, fixed by #75). (read-from-source — issues #74, #75)
  • Help coverage once drifted from the dispatch table (historical): man, completion, serve, openapi, adapter, registry, and api were once dispatched by cmd/mcpx/main.go but undeclared in internal/cli/manual.go, so help, the man page, and completions did not list them. All seven are declared now, and TestEveryDispatchedCommandIsDeclared holds the declaration and the dispatch table equal in both directions, so the gap cannot reopen silently. (read-from-source — docs/cli.md, “once dispatched but undeclared”; internal/cli/manual.go)

Containers and CI

Outline only in this slice: the container and CI seeds (static Go builds, image-size anatomy, actions/setup-go in a monorepo, stopping test daemons) are harvested with the Nix slice below, because several were recorded alongside it (#182) and splitting them twice would fork them. The seeds themselves are listed on #1871 and in docs/container.md sections 4–5.

Nix

Later slice, named rather than dropped. The standalone Nix section — a section that does not need to mention mcpx — is the second slice of #1871, not part of this one. Its seeds are already recorded on #1871: vendorHash repair in the mcpx workflow, the sandbox-test package list in package.nix, pure-Go SQLite, wrapProgram PATH suffixes, generated man pages and completions, the flake layout and patch maintenance for this repository, and the macOS findings. Open question 2 on #1871 (does this section live in this book, or in a book for the whole repository that this one links to?) is still open and should be answered before the slice is written, not after.

Harvesting: how findings get here

Approach C from #1871, in force for this chapter:

  1. Intake. Anything learned that is not about the change at hand — a verified surprise, a measured cost, a behaviour read in someone else’s source — is filed as an issue (or a comment with a link) under the research label, not buried in a session transcript or a PR body. A finding stays an issue until it is written up here, so nothing is lost in between. docs/ideas.md states the same discipline for design questions: an answer that only exists in a conversation is lost.
  2. Entry format. A finding written up here is an excerpt plus a citation, in the entry style the runbook uses (created / last-updated / status / tags / sources / checked), with the finding-specific statuses verified, measured, read-in-source, reported, hearsay, and superseded. The tags in this file’s key block are the chapter’s; a finding’s own topics go in its section placement and its citation.
  3. Harvest. A per-release step moves labelled items into this chapter as entries and closes the issues with a link to the section that took them. Excerpt, do not move: a finding inside a feature entry keeps that entry as its source of truth and is cited from here.
  4. Supersede, never silently edit. When a finding turns out wrong, it is struck through or marked superseded with a link to what replaced it. The history is the audit trail (#150).

Quality bar for an entry

From #1871, applied to every seed above:

  • Sources. Every claim has at least one: a file with a section or line, a commit, an issue, or an external URL with the date it was read.
  • Measurements. A measurement carries its environment (machine, OS, runtime and tool versions) and the command that produced it — re-run the command, paste the result.
  • Provenance. Say how it is known: reproduced, read in source, read in docs, or someone said so. Never let hearsay read like a measurement.
  • Dates. Every entry is dated; a re-check updates the date. The seed harvest above was checked against main on 2026-11-01.
  • Standalone. Where possible an entry is readable without mcpx context. That is the point of the Nix section.

Script runtimes, permission profiles and presets

mcpx run and mcpx exec hand the generated program to a JavaScript runtime. This page covers which runtime runs it, what it is allowed to do, and how to bundle mcpx’s own flags under a name. Every setting named here is in configuration.md with its flag and variable.

Choosing a runtime

script.runtime (--runtime, MCPX_SCRIPT_RUNTIME) is one of:

ValueMeaning
auto (default)the first entry of script.runtimeOrder that is installed and can enforce the permissions asked for
deno, bun, nodethat binary on PATH
a name under script.runtimesthe declared binary, with the declared kind
a path or another name on PATHaccepted when its basename, without an executable extension, is deno, bun or node

Binaries are resolved with Go’s exec.LookPath, so PATHEXT is honoured on Windows and nothing assumes .exe. A name that resolves to something that is not a JavaScript runtime is refused, saying so:

mcpx: bash resolves to /bin/bash, which is not a known JavaScript runtime; declare it under script.runtimes with a kind (deno, bun, node)

Declaring runtimes

{
  "script": {
    "runtimes": {
      "mydeno": { "kind": "deno", "bin": "/opt/deno/bin/deno", "args": ["--unstable-kv"] },
      "node22": { "kind": "node", "bin": "node22" }
    },
    "runtimeOrder": ["mydeno", "bun", "node"]
  }
}

kind decides how the command line is built; bin is a path or a name looked up on PATH (default: the declared name); args are runtime options placed after mcpx’s own fixed options and before the permission flags:

Kindargv
denorun --quiet --no-check <args> <permission flags> <script>
bunrun <args> <permission flags> <script>
node--no-warnings --experimental-transform-types <args> <permission flags> <script>

The script sees MCPX_RUNTIME set to the kind, not the declared name.

script.runtimes and script.profiles are single settings holding a JSON object, so the nearest file that sets one replaces the whole object from files further away, as with any other setting.

Permission profiles

script.permissions (--permissions, MCPX_PERMISSIONS) names profiles. A profile maps a runtime kind to the flags that enforce it there. The built-ins live in internal/defaults/defaults.json (mcpx config --defaults):

Profiledenonodebun
all (also none, off, unsandboxed) — the default--allow-allno flagsno flags
net--allow-net --allow-env——
read--allow-read --allow-env——
readnet (also read-net)--allow-read --allow-net --allow-env--permission --allow-fs-read=*—
strict--allow-net=127.0.0.1 --allow-env——

— means refused. A runtime kind a profile does not list cannot run under it, and mcpx says which profile, which runtime, and how to define it:

mcpx: permission profile "strict" defines no flags for node, so node cannot enforce it (it is defined for deno); use --runtime deno, or define script.profiles.strict.node

Under auto, a runtime that cannot enforce the profile is skipped rather than used, so auto never falls back to a runtime that would ignore it (decisions/0003).

What each runtime can actually enforce

  • deno has a complete permission model; every profile above is enforced.
  • node (checked against v24) has --permission, which denies file-system reads and writes, child processes, worker threads, addons, WASI and the inspector unless re-allowed with --allow-fs-read, --allow-fs-write, --allow-child-process, --allow-worker, --allow-addons, --allow-wasi. It has no network restriction, and it needs read access to load the script and the generated client. So node can genuinely enforce readnet (read anything, network open, no writes, no subprocesses, no workers) and nothing narrower in the network or read dimension; net, read and strict are therefore not defined for it.
  • bun has no permission model at all. Only all is defined for it, and nothing you could define for it would be enforced.

Defining and overriding profiles

{
  "script": {
    "profiles": {
      "strict": { "deno": ["--allow-net=localhost", "--allow-env"] },
      "tmpwrite": { "deno": ["--allow-write=/tmp"], "node": ["--allow-fs-write=/tmp"] },
      "mine": "--allow-read=/srv --allow-env"
    }
  }
}

A user profile replaces a built-in of the same name entirely. An object maps kinds to flag lists; a string is raw flags used for every kind. Every entry must be a flag (start with -) — a bare path is refused rather than handed to the runtime as if it were the script.

Composition

--permissions read,strict,tmpwrite applies profiles in the order given:

  • the result is the union of their flags;
  • where two give the same flag (the text before =) with different values, the later one wins, kept in the position the flag first appeared;
  • if any named profile has no entry for the runtime’s kind, the whole run is refused, naming that profile.

So for deno read,strict is --allow-read --allow-env --allow-net=127.0.0.1, and strict,net ends with --allow-net (unrestricted) because net came later.

Raw flags

raw: passes flags through verbatim and takes the rest of the value, commas included, so it goes last:

mcpx exec --permissions 'strict,raw:--allow-read=/a,/b --allow-write=/tmp' '...'

Raw flags are passed to whichever runtime runs, unchecked against its kind. A value that looks like flags but is not prefixed with raw: is refused with the explicit spelling, rather than being guessed at.

Presets

A preset is a named list of mcpx’s own flags:

{
  "presets": {
    "ci": ["--json", "--log-level=warn"],
    "debug": ["--log-level=debug", "--timeout=30s"]
  }
}

Select with --preset ci,debug (or --preset=ci,debug), MCPX_PRESET, or "preset" in a config file. Rules:

  • presets apply in the order named; a later preset wins over an earlier one;
  • any flag given explicitly on the command line wins over every preset;
  • presets sit between the environment and the command line in the settings precedence (default < file < env < preset < flag < runtime override); preset definitions under presets are content, not unknown keys;
  • each entry is one token: write values as --flag=value;
  • a preset applies those of its flags the running command accepts and skips the rest, because one preset is meant to serve several commands;
  • a preset cannot select other presets.

Values a preset supplied are shown as such: mcpx settings and mcpx settings get report preset:ci (--log-level) as the source, with the presets and defaults it overrode, and mcpx config --sources lists them under FROM PRESET.

Schema validation (internal/schemaval)

mcpx validates tool and elicitation schemas with the internal/schemaval package: a JSON Schema 2020-12 validator (wrapping github.com/santhosh-tekuri/jsonschema/v6, its only import site in this module) with an explicit dialect policy. It exists so Phases 2–3 of the schemaval design can compile schemas at list time and enforce them at call time; the core package itself changes no call paths.

Division of labour: internal/mcpspec validates MCP protocol frames and is test-only; schemaval validates tool/elicitation schemas and is the validator the binary carries.

Dialect policy

Declared $schemaResult
(absent)Treated as 2020-12
2020-12 (https://json-schema.org/draft/2020-12/schema)Supported
2019-09 (https://json-schema.org/draft/2019-09/schema)Supported
draft-07 (http://json-schema.org/draft-07/schema#)Supported
draft-06 (http://json-schema.org/draft-06/schema#)Supported
draft-04 (http://json-schema.org/draft-04/schema#)Supported
anything elseCompile fails with a typed UnsupportedDialectError, distinct from a generic compile error

Rules

  • No network, ever. The compiler’s loader refuses every URL. A schema may reference only itself (internal $refs into its own $defs and the like, or its own $id). Any other reference — internal-but-missing or external — fails compilation with a typed UnresolvedRefError. This is the json-schema-no-network-ref conformance rule, enforced at compile time.
  • Size bound. A schema document larger than 1 MiB (1,048,576 bytes) fails compilation with a typed SchemaTooLargeError. The largest legitimate schema documents in this repo (the vendored protocol schemas) are under 200 KiB.
  • Formats are annotations, not assertions, in v1. A value that violates a format keyword still validates; format checking may be turned on deliberately in a later phase, never by accident.
  • Integer fidelity. Instances are decoded without going through float64, so integers past 2^53 validate exactly (bounds, multipleOf, enum membership).
  • Errors are flat and path-carrying. A failed validation returns one line per problem in the form $.count: got string, want integer, so a caller can show a tool user exactly what to fix.

Output policy: structuredContent is warn-only

Tool inputs are enforced: internal/mcpclient validates CallTool arguments against the tool’s inputSchema and rejects invalid arguments before the transport (error -32602; design #1481, Phase 2 #1483).

Tool outputs are deliberately warn-only. checkToolOutput (internal/mcpclient/schemacheck.go) validates a result’s structuredContent against the tool’s outputSchema, but a violation does not change the result: it is delivered unchanged and a Warning (“structuredContent violates outputSchema (delivered unchanged)”) is recorded instead.

Rationale: mcpx is a gateway for servers it does not control. Rejecting an otherwise usable result because a third-party server’s output drifts from its declared outputSchema would break working integrations for a documentation defect in someone else’s schema; a warning keeps the drift visible without making mcpx the enforcement point. Flipping outputs to enforcing is a separate decision, not a default that may be turned on by accident (see the design in #1481 and Phase 2 in #1483).

Scripts

Script

A script is TypeScript (or JavaScript) that calls tools through the generated client: no MCP boilerplate, no schemas in context, just tools.<ns>.<fn>(args) — or the namespace as a bare identifier inside mcpx exec. This is the page a script writer lives on: the contract a script runs under, where named scripts live, which runtime runs them, and how to check one before it runs. The full contract is also stated in AGENTS.md, “Script contract”; this page adds the mechanics around it.

The contract

  • stdout is the result; stderr is diagnostics. Use the log helper rather than console.error for diagnostics: log.info("fetched {count} pages", { count: 3 }) — a message may carry {placeholders} filled from the attributes, and log(msg) is log.info(msg).
  • A default export is the entry point. If a script has one, mcpx calls it with argv and prints what it returns. Otherwise top-level code runs. --export <name> calls a named export instead, spreading the arguments into it.
  • emit() streams. Each value passed to emit() is written as it is produced; streaming and returning combine — the streamed values are progress, the return value is the answer.
  • --json gives one envelope. mcpx --json run <script> returns one document with the result, the logs, the exit code and the duration — the form another program should consume.
  • Results arrive unwrapped. Structured output is already parsed, JSON-in-text is already parsed, plain text is a string. .raw holds the full MCP envelope (that is where image bytes live). A tool returning isError throws ToolError.
import tools, { log } from "./mcpx-client.ts";

export default async function main(args: string[]) {
  const pages = await tools.chrome_devtools.list_pages({});
  log.info("fetched {count} pages", { count: pages.length });
  return pages;
}

Scripts need no imports at all inside mcpx exec: tools, log, emit and every namespace are already globals. Relative paths in a script resolve against your working directory, not the script’s.

Named scripts

A named script lives at .mcpx/scripts/<name>.ts, searched upward from the working directory, then ~/.config/mcpx/scripts/. The nearest one wins.

mcpx scripts              # list what exists
mcpx run <name> [args…]   # run one
mcpx run ./path/to.ts     # a path is used verbatim

mcpx scripts lists shadowed scripts too — a nearer script of the same name hides a farther one, and the listing marks which is which rather than letting the hidden one surprise you. Arguments after the name arrive in Deno.args on Deno; on every runtime, a default export is called with argv, which is the portable way to read them.

Runtimes

script.runtime (--runtime) chooses; the default order is deno, then bun, then node — the first in script.runtimeOrder that is installed and can enforce the permission profile asked for. A runtime that cannot enforce the profile is skipped under auto, never silently weakened. runtimes.md is the full page on choosing, declaring and constraining runtimes; the two limits a script writer hits first:

  • Node transforms TypeScript; it does not merely strip it. Node is invoked with --experimental-transform-types, chosen over strip-only mode precisely because strip mode rejects non-erasable TypeScript — enum, value namespaces, parameter properties — that Node itself runs fine once types are transformed. An enum in a script is guarded by a test (TestNodeRunsNonErasableTypeScript), not a limitation.
  • Typechecking is Deno-only. --typecheck / script.typecheck runs the check (via deno check) before the run and only applies when Deno is the runtime.

Authority

Scripts run with your full permissions: a real runtime, the real filesystem, the network, subprocesses. script.permissions names narrower permission profiles, but a profile is only as real as the runtime enforcing it — Deno enforces them, node enforces some, bun enforces none — and under auto a runtime that cannot enforce the profile asked for is skipped rather than trusted. The details and the profile table are in runtimes.md. The short version is the FAQ’s answer to “is it safe?”: no — treat a script like a shell command you are about to paste.

Where a script runs

exec.where is auto, local or remote (--local, --remote). Auto runs locally when the daemon is on this machine and on the daemon when it is not — a person at a terminal gets a terminal-shaped run, and a configured remote endpoint gets a remote one. How artifacts (a screenshot a script produced, say) are delivered from each arrangement is exec.md’s subject.

Checking before running

  • mcpx diagnose checks a script before it runs — most valuably against the daemon’s record of what each tool’s schema used to be, so a call broken by a schema change is reported as two facts (“the argument became required; you wrote none”) rather than as a bare failure at run time.
  • Automatic preflight (diagnose.preflight) runs those checks on exec source before it executes. A diagnostic is advice with a name: --diagnose-preflight=false runs the script anyway.

Recipes, and onward

A recipe is a named script that declares its own holes in its header:

// @param repo:string   which repository
// @param days:number = 30   how old counts as stale

mcpx recipes lists them; mcpx recipes run <name> key=value… fills each @name reference in the body with the value, encoded as JSON, so a recipe cannot become an injection site. Values not supplied and without a default are asked for as one form. mcpx recipes match '<words>' ranks recipes against free text deterministically — a score over names, descriptions, parameters and the tools called; no model, no cost, the same answer every time.

Onward from here: mcpx prompt turns a request in words into a script (trying recipes first, and returning the script rather than running it unless --run is given, because generated code runs with your credentials); artifacts a script produces are covered in exec.md; and stateful servers — why two calls in one invocation share a browser and two invocations may not — are under “Stateful servers” in AGENTS.md.

mcpx

A user types a prompt. An agent that has never heard of mcpx works out what it is, writes one script, and hands back two screenshots. This is that walk, with the exact bytes it saw.


At 09:14 a user opens a blank session and types:

Take a screenshot of example.com, click the link on it,
wait for the new page to load, and screenshot that too.

The agent has no MCP servers configured. Its tool list is the usual built-ins — read, write, edit, bash, glob, grep. Nothing named “browser”, nothing named “chrome”. There are 27 Chrome tools live on this machine and not one byte of their schemas is in the agent’s context, which is the entire point, but the agent does not know that yet.

What it does have is one instruction file, AGENTS.md, injected at session start. Abridged – the full file is 152 lines and adds profiles, the script contract and the secrets section, which this walk does not need – it reads:

# mcpx

Every MCP server on this machine is reachable from the shell through `mcpx`.
None of their tool schemas are in your context. You pull in only what you need.

## The loop

1. `mcpx ls` — which namespaces exist. Cheap; run it whenever.
2. `mcpx search <words>` — find a tool by name or description.
3. `mcpx types <namespace>` — load signatures for that namespace only, or
   `mcpx types <namespace>.<tool>` for a single tool, which is far cheaper.
   `mcpx catalog [--budget N] [--bias words]` fits every namespace into a
   token budget when you are still deciding.
4. `mcpx run <name>` or `mcpx exec '<code>'` — do the work.

Do not run `mcpx types` for every namespace. Loading one costs a few hundred
tokens; loading all of them costs thousands and defeats the point.

## Scripts

A named script lives at `.mcpx/scripts/<name>.ts`, searched upward from the
working directory, then `~/.config/mcpx/scripts/`. Nearest wins.

    mcpx scripts                 list what exists
    mcpx run <name> [args...]    run one; args arrive in Deno.args
    mcpx run ./path/to/file.ts   a path is used verbatim

Scripts are TypeScript on a real runtime (deno, else bun, else node). They get
the full runtime: filesystem, network, subprocesses. Relative paths resolve
against _your_ working directory, not the script's.

`log`, `emit`, `tools` and every namespace are already on `globalThis`, so a
script needs no imports at all. `log(msg)` is `log.info(msg)`. Importing works
too and gives the same objects:

    import tools from "./mcpx-client.ts";
    const pages = await tools.chrome_devtools.list_pages({});

Every namespace is also a bare identifier inside `mcpx exec`.

## Results

A result arrives unwrapped: structured output already parsed, JSON-in-text
already parsed, plain text as a string. `.raw` holds the full MCP envelope —
that is where image bytes live. A tool returning `isError` throws `ToolError`.

## Stateful servers

`mcpx ls` has SHARING and SCOPE columns. SCOPE says what a server process is
keyed by; SHARING says whether one process serves several callers at once.

A namespace scoped to `session` gives each session its own process. If the host
set `MCPX_SESSION_ID`, successive runs in that session reach the same process,
so a browser persists between invocations. If it did not, each run is isolated
and anything that must share state — navigate, snapshot, click — has to happen
inside **one** invocation.

`mcpx status` shows the KEY each live process is serving, which is the quickest
way to see whether you are sharing or not.

## When something is wrong

- `error` in `mcpx ls` — `mcpx status` has the server's stderr.
- Arguments rejected — `mcpx types <ns>` is generated from the server's own
  schema and is authoritative.
- Wedged server — `mcpx restart <namespace>`.

## Do not

- Do not add these servers to your own MCP config. The point is that they are
  not in your context.
- Do not parse human output; use `mcpx --json <command>`.
- Do not assume a tool name is a valid identifier: `fancy-name` becomes
  `fancy_name`. `mcpx types` shows the real function name.

The first three lines do all the framing. Reachable from the shell through mcpx means this is a command, not a tool call and not a plugin — it belongs in bash. None of their tool schemas are in your context explains the absence the agent just noticed. You pull in only what you need says the absence is deliberate and reversible.

From those three lines the agent now knows:

  • capability it cannot see exists, and mcpx is the door to it;
  • discovery is a separate, cheap step from use;
  • loading everything is the failure mode the file is warning against.

That last point is what stops it reflexively fanning out an explorer subagent across ~/.config, /usr/local/bin and the repo looking for browser tooling. The file already said where to look. Twelve lines of guideline follow, and they are all shaped the same way — a command, then the cost of misusing it.

So the agent makes one pinned memory:

mcpx — MCP via shell, not tools. ls → search → types <ns> → run. Scripts: .mcpx/scripts/<name>.ts, import ./mcpx-client.ts. Stateful namespaces need one invocation. Never types everything.

and three todos:

  1. mcpx ls — confirm a browser namespace exists and note its mode
  2. mcpx types for that one namespace only
  3. write .mcpx/scripts/shot-flow.ts, run it, report paths

Todo one. The agent runs mcpx ls and gets back, in 10 milliseconds, without a single MCP server process being started:

NAMESPACE        TOOLS  SHARING    SCOPE    LIVE  STATE  DESCRIPTION
chrome_devtools  27     exclusive  session  0     ready  drive a headless Chrome
codebase_memory  0      shared     global   0     error  server "codebase-memory": initialize: mcp error -32000: con…
codedb           5      shared     global   0     ready  where a symbol is defined, who calls it
context7         2      shared     global   0     ready  third-party library docs
fff              3      shared     global   0     ready  ranked search over ~/git
fff_nix          3      shared     global   0     ready  ranked search over ~/.config/nix
fff_worktree     3      shared     global   0     ready  ranked search over opencode worktrees

Next: `mcpx types <namespace>` for signatures, `mcpx search <query>` to find a tool.

Seven namespaces, 43 tools, 171 tokens. chrome_devtools is the one. Its SCOPE is session and its SHARING is exclusive, which the instructions already told it means one browser per session, one caller at a time — so unless the host set a session id, do it all in one invocation. LIVE 0 means nothing is running yet. One row says error, and it is honest about why — that server is genuinely broken today and mcpx says so instead of quietly omitting it.

The agent does not yet know which of the 27 tools it needs, so it narrows first:

$ mcpx search screenshot click
FUNCTION                         DESCRIPTION
chrome_devtools.click            Clicks on the provided element
chrome_devtools.take_screenshot  Take a screenshot of the page or element.
chrome_devtools.fill_form        Fill out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS pre…
chrome_devtools.take_snapshot    Take a text snapshot of the target page based on the a11y tree. The snapshot lists page e…

Sixty-one tokens. Four names, enough to know the shape of the job.

Todo two. mcpx types chrome_devtools — 4,402 tokens, the single largest thing the agent will load all session, and it loads it once and deliberately:

// Generated by mcpx. Call these from a script run with `mcpx run <file.ts>`.
// Every function is async and returns the tool's result.

/** drive a headless Chrome */
declare namespace chrome_devtools {
  function click(args: {
    /** Set to true for double clicks. Default is false. */
    dblClick?: boolean;
    /** Whether to include a snapshot in the response. Default is false. */
    includeSnapshot?: boolean;
    /** Targets a specific page by ID. */
    pageId: number;
    /** The uid of an element on the page from the page content snapshot */
    uid: string;
  }): Promise<ToolResult>;

This is generated from Chrome’s own JSON Schema, so it is authoritative rather than remembered. Two facts land that the agent could not have guessed: pageId is required on nearly every call, and click takes a uid “from the page content snapshot” — not a CSS selector. Those two constraints determine the whole shape of the script. An agent working from memory of how browser automation usually looks would have written click({ selector: "a" }) and failed twice before finding out.

Todo three. The script. Convention says .mcpx/scripts/<name>.ts, so:

// Screenshot a page, click its first link, wait for load, screenshot again.
//
// Usage: mcpx run shot-flow [url] [outDir]
import tools from "./mcpx-client.ts";

const cd = tools.chrome_devtools;
const url = Deno.args[0] ?? "https://example.com";
const outDir = Deno.args[1] ?? "./shots";
await Deno.mkdir(outDir, { recursive: true });

async function shot(pageId: number, name: string): Promise<string> {
  const result = await cd.take_screenshot({ pageId });
  const image = ((result.raw as any)?.content ?? []).find((c: any) => c.type === "image");
  const path = `${outDir}/${name}.png`;
  await Deno.writeFile(path, Uint8Array.from(atob(image.data), (c) => c.charCodeAt(0)));
  return path;
}

await cd.new_page({ url });

// list_pages prints "<id>: <title> (<url>)"; find the one we just opened.
const pages = String(await cd.list_pages({}));
const host = new URL(url).host;
const row = pages.split("\n").find((line) => line.includes(host) && /^\d+:/.test(line));
if (!row) throw new Error(`no page for ${host} in:\n${pages}`);
const pageId = Number(row.split(":")[0]);

const before = await shot(pageId, "1-before");

// The a11y snapshot gives every element a uid; click needs that, not a selector.
const snapshot = String(await cd.take_snapshot({ pageId }));
const uid = snapshot.match(/uid=(\S+) link "/)?.[1];
if (!uid) throw new Error(`no link in snapshot:\n${snapshot}`);

await cd.click({ pageId, uid });
await cd.wait_for({ pageId, text: ["IANA", "Example"] });

const after = await shot(pageId, "2-after");
console.log(JSON.stringify({ url, clicked: uid, before, after }, null, 2));

Three things in there are only possible because this is a real runtime and not a sandbox. Deno.mkdir and Deno.writeFile do actual filesystem work. atob decodes the PNG out of .raw, because the image bytes never pass through the model. And new URL(url).host is just the standard library, present because this is ordinary TypeScript.

The whole browser conversation — new_page, list_pages, take_screenshot, take_snapshot, click, wait_for, take_screenshot — is seven tool calls and a multi-kilobyte a11y tree. None of it reaches the agent. Only the last console.log does.

$ mcpx run shot-flow
{
  "url": "https://example.com",
  "clicked": "1_3",
  "before": "./shots/1-before.png",
  "after": "./shots/2-after.png"
}

Under a second of agent-visible work, and a browser that started, did its job, and was released when the process exited. Had a second agent run the same script at the same moment, it would have gotten its own browser and its own 1_3, because session mode leases a process per run.

The agent replies:

Both screenshots are saved.

  • ./shots/1-before.png (25 KB) — example.com as loaded
  • ./shots/2-after.png (100 KB) — iana.org, after clicking the “Learn more” link (uid=1_3)

Reusable as mcpx run shot-flow <url> <outDir>.

The user opens both, sees the plain example.com page and then the IANA page, and notices the interesting part: the agent spent about 4,600 tokens total, nearly all of it on one deliberate types call, and left behind a script that costs nothing to run again.


What the user turns over next is not whether it worked but where the edges are. If a script can return primitive values, can a plain one skip the browser entirely and just print a number? Sessions are leased per run — could one be named, pinned, listed, searched, and re-attached later, read-only for watching or read-write for driving? Could a still-cached session be resumed instead of rebuilt? Could the daemon’s log answer questions about what ran and how long it took? A wedged browser can be restarted, but can it be restarted for one session rather than the whole namespace? Can the daemon be upgraded without dropping the servers it holds? And how should this be wired up at all — a launchd job, a systemd unit, a shell function, a completion script? What about driving it interactively, or from CI with the script on stdin, or with everything in environment variables?

Those are the questions the reference pages exist to answer, so the user opens them, and over time learns about:

Zero-context discovery

id:           zero-context-discovery
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       core
tags:         area:discovery, cost:context
description:  ls, search and types answer from an on-disk cache without
              starting a single MCP server process.

mcpx ls, mcpx search and mcpx types never touch a server. They read a schema cache keyed to the config file’s fingerprint, so a cold daemon answers in about 10 ms and a warm one in about 1 ms.

The cache is written to $XDG_CACHE_HOME/mcpx/schemas-<hash>.json and survives daemon restarts, machine reboots, and the MCP server binary being deleted — there is a regression test that moves the binary away and checks mcpx types still answers.

The practical effect is that an agent can run mcpx ls as often as it likes. The cost model is: ls about 171 tokens, search about 61, types per namespace from 675 (codedb) to 4,402 (chrome_devtools). Nothing is loaded that was not asked for.

mcpx refresh re-reads every server. mcpx --json ls is the machine-readable form; never parse the table.

2026-09-27T05:05:00-05:00

Sharing and scope

id:           sharing-and-scope
since:        pre-release
created:      2026-09-25T18:30:00-05:00
last-updated: 2026-09-27T06:20:00-05:00
status:       core
tags:         area:pools, area:concurrency
description:  two independent axes -- how many callers share a process, and
              what decides which process you get.

A single mode setting used to answer both questions at once, which meant neither could be chosen freely. They are now separate.

sharing — how many callers may use one process at a time.

valuemeaning
shared (default)any number of concurrent callers; MCP multiplexes by JSON-RPC id
exclusiveone caller at a time, others queue

scope — what a process is keyed by. One live process per distinct key.

scopekeyresolved by
global (default)constantmcpx
repogit rev-parse --git-common-dirmcpx
worktreegit rev-parse --show-toplevelmcpx
cwdworking directorymcpx
sessionMCPX_SESSION_ID or --sessioncaller
parent-sessionMCPX_PARENT_SESSION_IDcaller
pidcalling process idcaller
callunique per invocationmcpx
"chrome-devtools": {
  "command": "chrome-devtools-mcp",
  "args": ["--headless", "--isolated"],
  "mcpx": { "sharing": "exclusive", "scope": "session", "max": 4, "idleTimeout": "5m" }
}

The defaults describe a stateless server, which most are, so a server with no mcpx block gets one shared process for everything.

What mcpx cannot work out for itself

repo, worktree, cwd and call are computed from the call itself. session and parent-session cannot be: a subagent and its parent share a working directory and differ only by an identifier their host assigns. The caller supplies those through MCPX_SESSION_ID and MCPX_PARENT_SESSION_ID.

A scope that cannot resolve degrades to per-call isolation and says so once, in the daemon log, naming the variable that would fix it. Degrading toward isolation is deliberate: accidentally sharing a stateful process corrupts results, while over-isolating only costs a process.

Verified end to end. Two invocations under one MCPX_SESSION_ID reach the same browser, so the second sees the page the first opened. Two subagents under one MCPX_PARENT_SESSION_ID share; a third under a different parent does not:

live= 2 keys= ['psession:root', 'psession:other']

Lifetime

pid-scoped processes are stopped as soon as the process they belong to exits, rather than waiting out an idle timer — there is no possible future caller. Everything else is reaped on the idle timer, except keys a caller minted for itself, which are stopped the moment that caller finishes.

2026-09-27T06:20:00-05:00

Profiles and aliases

id:           profiles-and-aliases
since:        pre-release
created:      2026-09-27T13:00:00-05:00
last-updated: 2026-09-27T13:00:00-05:00
status:       standard
tags:         area:config, cost:context
description:  select subsets of servers by profile, and expose one server
              several times under different namespaces and tool subsets.

Profiles decide which servers a command sees at all. A browser nobody is using should not occupy a namespace, a row of mcpx ls, or a share of a catalogue budget.

"chrome-devtools": { "mcpx": { "profiles": ["web"], "default": false } }
mcpx ls                                 the default set
mcpx --profile web ls                   default set plus the web servers
mcpx --profile web --skip-default ls    exactly the web servers
mcpx --all-profiles ls                  everything, ignoring profiles

A server is default-on unless it says otherwise; "pool": { "default": false } flips the baseline so servers opt in instead. The selection applies to everything derived from the server list — ls, types, catalog, search, and the generated client — so a script written under one profile cannot reach a namespace outside it.

Aliases expose one server under a second namespace with its own tool subset, description and prelude.

"chrome-peek": {
  "aliasOf": "chrome-devtools",
  "mcpx": {
    "sharing": "exclusive", "scope": "session",
    "tools": ["list_pages", "take_snapshot", "take_screenshot"],
    "description": "read-only view of the same browser"
  }
}

Whether an alias shares a process with its target depends on whether its leasing matches. Pool identity covers the command, arguments, environment, working directory, transport, sharing, scope, maxima and timeouts — everything that changes the process or how it is handed out. Tool filtering is not in it, because filtering is presentation.

So the example above shares one browser with chrome_devtools: opening a page through the full view and listing pages through the restricted one shows the same page, from the same pid. Change the alias’s scope and it becomes a separate process instead. mcpx status reports a shared pool once, under every name that reaches it.

Chains are rejected. An alias of an alias is a puzzle, and the error says to point at the original.

2026-09-27T13:00:00-05:00

The script contract

id:           the-script-contract
since:        pre-release
created:      2026-09-27T14:30:00-05:00
last-updated: 2026-09-27T14:30:00-05:00
status:       standard
tags:         area:scripts, area:logging
description:  stdout is the result, stderr is logs, a default export is the
              entry point, and --json wraps the lot.

stdout is the result. stderr is diagnostics. A script that prints nothing to stdout returns nothing.

A default export is an entry point. If a module has one, mcpx calls it and prints whatever it returns; if it does not, top-level code runs on import as before. One file is therefore both importable and runnable without ceremony:

import tools, { log } from "./mcpx-client.ts";

export function countFiles(query: string) { /* importable */ }

export default async function main(args: string[]) {
  log.info("searching for {query}", { query: args[0] });
  return { count: await countFiles(args[0]) };     // printed as JSON
}

main receives argv as an array, the way every other main does. mcpx run --export countFiles script alpha calls a named export instead, with arguments spread — --export f a b reads as f(a, b). Naming an export that does not exist lists the ones that do.

Streaming results. A return value is one answer at the end; emit() is many answers as they are found:

for (const file of files) emit({ file, findings: await scan(file) });

Each value is written immediately — one JSON line on stdout in ordinary use, or collected in order into the envelope’s results array under --json. A script can stream and return: the streamed values are the progress, the return value is the conclusion.

Logging is available to scripts and shares the daemon’s renderer:

log.info("fetched {count} pages in {ms}ms", { count: 3, ms: 412, url });
19:44:45.911 INFO  fetched 3 pages in 412ms url=https://example.com

A message may carry {placeholders} filled from the attributes; {{x}} writes a literal {x}. Both forms are kept: the interpolated message for reading, the template for grouping records that differ only in their values. An attribute consumed by the template is not repeated in the trailing key/value list. A missing placeholder is left visible rather than blanked, because a hole in a sentence is a bug worth seeing.

The first argument after the message becomes attributes when it is a plain object. Anything else, and anything after it, is collected into an args array, so console-style calls keep their values instead of dropping them. An Error is captured as name, message and stack rather than stringified:

log.error("upload failed", err);            // err becomes args[0], structured
log.debug("state", { id }, "extra", 42);    // id is an attribute

Call sites are recorded for warn and above by default. Capture costs about 5 microseconds in a script – measured, and roughly fifty times the cost of the record it decorates – because building the stack trace is the expensive half. That is worth paying where something went wrong and wasteful on routine progress, so the default traces the levels you would actually investigate. --log-source alone widens it to everything, --log-source=error narrows it, --log-source=false turns it off.

A filtered call costs nothing. The script knows the active threshold, so log.debug() below it returns after a comparison: 0.026 microseconds measured, against 0.10 for the encode-and-write it used to do and 5 for a traced one. Debug logging can be left in.

Binding context works as it does in slog:

const scoped = log.with({ run: runId, phase: "scan" });
await scan(scoped, file);

The returned logger has to be passed where it is needed. JavaScript has no ambient context, so bindings do not follow the call stack by themselves; the alternative would be a hidden global, which is worse than an explicit argument.

Records travel on stderr behind a U+001E marker rather than through the daemon, so logging works with no daemon reachable, costs no round trip, and cannot reorder against the script’s own output. Anything else on stderr passes through untouched.

Formats, on --format, for both scripts and the daemon:

text (default)19:44:45.911 INFO fetched 3 pages url=x
logfmtts=… level=info msg="fetched 3 pages" count=3
jsonone object per line, with msg and template
json-prettyindented
compactINFO fetched 3 pages
barefetched 3 pages

--log-level sets the threshold; MCPX_LOGGING_FORMAT and MCPX_LOG_LEVEL set defaults. The daemon renders through the same writer, so one choice governs everything.

Imports are optional. The launcher installs the standard surface on globalThis before importing a script, so a one-liner needs no imports:

export default async function main() {
  log("starting");                       // log() is log.info()
  emit({ partial: 1 });
  return await fff.grep({ query: "TODO" });
}

Importing still works and yields the same objects, which is what an editor wants. mcpx also writes mcpx-globals.d.ts beside the script so a language server knows the globals exist without an import.

console is captured. console.error, warn, info and debug become records, so they get enrichment, formatting and the durable log instead of being bare text on stderr. console.log is left on stdout – it is the script’s result and redirecting it would change what a caller reads – but is also recorded, marked file-only so the terminal does not show it twice. --no-capture-console turns all of this off.

Wrapping a run. --prefix and --suffix add lines around a script without the script knowing. For a file they run in the generated launcher — before the module is imported and after its entry returns, in a finally so cleanup survives a throw:

mcpx run --prefix 'log.info("starting {name}", { name: script.name });' \
         --suffix 'log.info("took {ms}ms", { ms: Math.round(result.ms) });' report

Prefix lines see script (path, name, args, export); suffix lines also see result (ok, value, error, ms). They can set globals the script reads, but cannot declare bindings inside its module scope — ESM does not allow it.

Lines layer across configuration: a list element of null (or - on the command line) splices in whatever was inherited, so a nearer config can extend a farther one instead of only replacing it.

--env KEY=VALUE sets variables for the run.

mcpx --json run wraps a whole run in one document — stdout, the parsed result, captured logs, the script’s own stderr, exit code, duration and runtime. Nothing leaks to the terminal alongside it:

{ "ok": true, "exitCode": 0, "durationMs": 97, "runtime": "deno",
  "result": { "count": 24 },
  "logs": [ { "level": "info", "msg": "searching for flake.nix",
              "template": "searching for {query}", "query": "flake.nix" } ] }

2026-09-27T14:30:00-05:00

Named scripts

id:           named-scripts
since:        pre-release
created:      2026-09-27T04:45:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:scripts, area:cli
description:  .mcpx/scripts/<name>.ts, searched upward from cwd then the user
              config directory; nearest wins.

mcpx run <name> resolves a bare name against, in order:

  1. ./.mcpx/scripts/<name>.ts, then the same path in each parent directory up to the filesystem root — so a repo’s scripts override a parent’s;
  2. $XDG_CONFIG_HOME/mcpx/scripts/<name>.ts;
  3. ~/.config/mcpx/scripts/<name>.ts.

Anything path-shaped — containing a separator, or ending .ts, .js, .mts — is used verbatim, so absolute paths and ./local.ts keep working unchanged.

mcpx scripts lists what is reachable, using each file’s first comment line as its description and marking entries shadowed by a nearer copy:

NAME       DESCRIPTION                                             PATH
shot-flow  Screenshot a page, click a link, wait for load, scree…  /tmp/demo/.mcpx/scripts/shot-flow.ts

Arguments after the name reach the script as Deno.args. The working directory is yours, not the script’s, so a relative output path means what it says on the command line.

2026-09-27T05:05:00-05:00

The generated client

id:           the-generated-client
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       core
tags:         area:codegen, area:types
description:  JSON Schema compiled to a typed TypeScript module written beside
              the script, so the runtime's own module cache applies.

mcpx compiles each tool’s JSON Schema into a TypeScript function and writes the module to mcpx-client.ts next to the script. Enums become literal unions, nullable types become unions with null, $ref is resolved, cycles bottom out in unknown, and descriptions become JSDoc.

Writing a real file is the reason scripts start in tens of milliseconds. The predecessor imported its client over HTTP with --reload, which re-downloaded and re-typechecked the module graph on every run and cost seconds.

Results arrive unwrapped: structuredContent parsed, JSON-in-text parsed, plain text as a string, everything else with .raw carrying the full envelope. isError throws a ToolError carrying server, tool and raw.

mcpx client -o <path> writes the module for a checked-in script so an editor can type-check it.

2026-09-27T05:05:00-05:00

Calls without a JavaScript runtime

id:           calls-without-a-javascript-runtime
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:cli
description:  mcpx call runs one tool with no script and no runtime, in ~47ms.
$ mcpx call fff_nix.find_files '{"query":"flake.nix","maxResults":3}'
$ mcpx --json call codedb.definition '{"symbol":"Registry"}' | jq -r '.content[0].text'

About 47 ms, no JS runtime involved. --raw prints the full MCP envelope; --session <key> joins an existing lease on a stateful server.

2026-09-27T05:05:00-05:00

One daemon per config

id:           one-daemon-per-config
since:        pre-release
created:      2026-09-26T00:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:daemon, area:multirepo
description:  the daemon is keyed to the config file it loaded, so repos do not
              share servers and a config edit takes effect immediately.

Without this, the first project to run a command would decide which MCP servers exist for every other project on the machine, and editing a config would leave a stale daemon serving the old one.

The key is a fingerprint of the config path plus its bytes. Different config, different daemon. Edited config, new daemon. Auto-started daemons exit after four idle hours; one started deliberately with mcpx daemon or under launchd stays up.

mcpx daemons lists them all, mcpx stop --all clears them.

On macOS the unix socket falls back to a short hashed path under $TMPDIR when the state directory would exceed the 104-byte sun_path limit, because the failure mode is otherwise an opaque bind: invalid argument.

2026-09-27T05:05:00-05:00

Runtime portability

id:           runtime-portability
since:        pre-release
created:      2026-09-25T19:40:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:runtime
description:  deno, then bun, then node; the generated client avoids syntax any
              of them reject.

--runtime deno|bun|node overrides detection; "runtime" in the config sets a default. Deno runs with --no-check, since the generated client is machine-written and a type error in the agent’s own script surfaces at runtime anyway.

The client is written to avoid TypeScript that Node’s strip-only mode rejects. A cross-runtime test caught constructor parameter properties failing on Node only, so the client uses plain fields. A second test asserts session isolation holds on all three, because a runtime that cannot read MCPX_SESSION would silently re-share every stateful server.

2026-09-27T05:05:00-05:00

Remote servers without a shim

id:           remote-servers-without-a-shim
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:transport
description:  Streamable HTTP and SSE are spoken directly; no mcp-remote
              process in between.
"context7": { "url": "https://mcp.context7.com/mcp", "transport": "http" }

Connects in about 800 ms with no Node shim process. Batched frames and SSE streams are both handled; Mcp-Session-Id is tracked and released with a DELETE on shutdown. OAuth is not implemented — see below.

2026-09-27T05:05:00-05:00

Honest failure reporting

id:           honest-failure-reporting
since:        pre-release
created:      2026-09-26T04:30:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:diagnostics
description:  a server that will not start is reported with its stderr instead
              of vanishing from the namespace list.
codebase_memory  0  shared  0  error  server "codebase-memory": initialize: mcp error -32000: con…

mcpx status has the full text, including the child’s stderr tail. Start failures enter an exponential cooldown capped at 30 s so a broken server cannot spin, and a failed schema fetch deliberately does not record a cache timestamp — otherwise a server that never started would persist as “cached, 0 tools” and the error would be lost across restarts.

Child processes are killed by process group, so browsers do not outlive the daemon.

2026-09-27T05:05:00-05:00

Search

id:           search
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-10-30T00:00:00-05:00
status:       standard
tags:         area:discovery
description:  ranked tool search, all-terms first with an any-term fallback.

Exact name match scores 100, prefix 60, a config tag 50, substring 40, a split/stemmed name word 40, function path 30, namespace 20, description 10. Names and terms are split on _ and camelCase boundaries and folded through a light suffix stemmer, so screenshots finds take_screenshot. Every term must match by default, which narrows well; when that returns nothing the query is retried as any-term, so mcpx search screenshot click returns both tools rather than neither.

A server’s config may declare tags (inherited by all its tools) and per-tool toolTags; --tag <tag> filters on them exactly, and catalog --bias counts a tag hit like a name hit. --explain prints the per-term, per-field breakdown behind each hit’s score.

-n <count> caps results, default 20.

2026-10-30T00:00:00-05:00

Lifecycle and operations

id:           lifecycle-and-operations
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:ops
description:  status, restart, refresh, stop, daemons, and the trace switch.
CommandEffect
mcpx status [-v]daemon, pools, live instances, pids, call counts, sessions — never starts a daemon; reports “not running” instead (#1866)
mcpx restart [<ns>]stop instances; the next call starts fresh ones
mcpx refreshre-read every server’s schemas
mcpx daemonsevery daemon for this user
mcpx stop [--all]shut down this config’s daemon, or all
mcpx daemon --port Nrun in the foreground
mcpx config [--path]resolved configuration
mcpx init [--global]starter config

Idle instances are reaped on a 30-second timer against each server’s idleTimeout. MCPX_TRACE=1 logs one line per tool call naming the instance that served it. mcpx stop flushes the schema cache before closing its listeners, so a stop followed immediately by a directory removal cannot race.

2026-09-27T05:05:00-05:00

Reading the log

id:           reading-the-log
since:        pre-release
created:      2026-09-27T21:00:00-05:00
last-updated: 2026-09-27T21:00:00-05:00
status:       standard
tags:         area:diagnostics
description:  mcpx log and mcpx stats, over a SQLite index of the JSONL files.

Every record the daemon and every script produce is already written as JSON lines under the state directory. That file is the record of truth and nothing else writes to it. Beside it sits index.db, a SQLite index built from it, and the only reason it exists is that questions like “which tool is slow” are queries, not greps.

The index is rebuilt by reading, not by a background thread. mcpx log and mcpx stats ingest before they answer, and ingest is incremental: a file whose size and mtime have not moved is not opened at all. A background indexer would be faster in the rare case and quietly wrong in the bad one, and an index nobody trusts is worse than no index. Deleting index.db costs a rescan and no data.

mcpx log --since 1h --level warn          recent trouble
mcpx log --event server.* --server fff    one server's lifecycle
mcpx log --grep 'timed out' --limit 50    regular expression over msg and attrs
mcpx log -f                               tail as records arrive
mcpx log --chain cal-9f3c...              a call and everything that led to it

--chain is the one worth knowing. A record carries the id of the thing it happened inside, and only the record that creates something carries its parent, so walking backwards from a tool call reaches the server instance that served it and then the daemon that started that. It prints as an indented tree, oldest first.

mcpx stats aggregates the same index. calls groups by server and tool with exact p50/p95/p99 — exact, not sketched, because these are thousands of rows and an approximation would trade the one property that matters, that the number printed is a call which really took that long. slowest prints individual calls with their trace ids, which is where --chain comes from. servers and instances cover process lifecycle, aggregate and one row per process respectively; errors groups on the message template rather than the interpolated text, so one recurring failure is one row; sessions says what each script run did; volume is records per level per hour plus what the logs cost on disk.

mcpx log sql '<select ...>' is the escape hatch, with --schema for the DDL and --path for the file. Anything that is not a read is refused. That is a guard rail rather than a security boundary: the index can be deleted and rebuilt at will, so it is protecting someone who typed DELETE meaning SELECT, not defending against anyone.

The driver is modernc.org/sqlite, a pure-Go translation of SQLite. A cgo driver would be faster and would also make the Nix build need a C toolchain and stop cross-compiling, which is a poor trade for an index.

2026-09-27T21:00:00-05:00

Configuration

id:           configuration
since:        pre-release
created:      2026-09-25T18:10:00-05:00
last-updated: 2026-09-27T05:05:00-05:00
status:       standard
tags:         area:config
description:  the same mcpServers object other MCP hosts use, plus an optional
              per-server mcpx block.

Searched upward from the working directory: .mcpx.json, .mcpx/config.json, .mcp.json; then $XDG_CONFIG_HOME/mcpx/config.json, ~/.config/mcpx/config.json, ~/.mcpx.json, /etc/mcpx/config.json. MCPX_CONFIG overrides everything. JSONC comments are stripped, string-literal aware.

Per server: mode, max, min, idleTimeout, callTimeout, startTimeout, namespace, description, tools, extraIncludeTools, excludeTools, disabled. A top-level pool block applies any of them to every server. The three tool lists take exact names, globs (delete_*) or /regexps/, and hide a tool from listings and calls alike; see Hiding tools.

2026-09-27T05:05:00-05:00

One declaration per setting

id:           one-declaration-per-setting
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:config
description:  a setting is described once and is then readable from a file, a
              variable and a flag.

internal/settings/registry.go holds every knob mcpx has. Each entry states a type, a default, a name, a sentence and a paragraph. From that one description the setting becomes readable from a configuration file at its dotted path, from a generated MCPX_ variable, and from a generated flag. There is no wiring, so there is nothing to forget to wire.

What this replaces had a flag in one file, a default in another, an environment lookup in a third and a struct field in a fourth. That has one failure mode and it happened every time: the four drifted, and “what is this set to” became “read all four and guess”. It also meant settings that were documented but unreachable – the plumbing switches could be set from the environment and nowhere else, so a configuration file could describe a value it could not apply.

Defaults are written as strings in the syntax a user would type and go through the same validator as an override, so a default cannot be invalid.

Precedence runs defaults, then configuration files with the nearest last, then the environment, then flags. Every value remembers what it overrode, because “why is this not what my config says” is the most common configuration question and the answer is now in the value itself.

Two spellings of one setting at the same level is an error. On a command line the same spelling twice is not – that is a person editing their own command, and the last one is what they meant. In the environment two variables disagreeing genuinely cannot be ordered, so it is refused rather than guessed.

mcpx config --schema prints the lot. --plumbing adds the internals.

2026-09-28T02:30:00-05:00

Plumbing

id:           plumbing
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:config
description:  internal switches for decisions that could reasonably go either
              way.

Settings under plumbing. control internals. They work, they are documented, and there is no ordinary reason to change one.

They exist because the code has guards that could defensibly go the other way. Whether a directory may stand in for a source string. Whether a bare argument naming a file is read as one. Whether foo.ts beside foo.js is an error. Rather than decide permanently and leave the other half of the world stuck, each guard reads a switch.

The cost is a longer list. The benefit is that nobody has to patch the binary to get past a decision that was never meant to be final.

They are hidden from ordinary help and left out of shell completion, because offering an internal in a tab list is how somebody sets one by accident.

2026-09-28T02:30:00-05:00

Source that can be a file

id:           source-that-can-be-a-file
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:scripts
description:  every string-shaped input also accepts a path, or a directory.

--prefix, --before, --on-error, --launcher and the rest accept inline source, a path to a file, or a directory whose files are concatenated.

The reason is that a snippet does not stay one line. The moment it grows, keeping it in a command is unpleasant and keeping it in JSON is unreadable. Making each setting accept either form costs one resolver; forcing the choice up front costs a rewrite at exactly the point the answer becomes obvious.

Detection is by probe: a value naming something on disk is read as it. @text: and @file: force the interpretation for the case where a snippet genuinely collides with a filename, and plumbing.sourceProbePaths turns the probe off.

A directory is concatenated in natural order, so 9 comes before 10. Byte order gets that backwards, which is wrong for precisely the case the feature serves – fragments numbered to control their order. Each file is named in a comment above its contents, because a stack trace into a concatenation is otherwise unattributable.

2026-09-28T02:30:00-05:00

The launcher is replaceable

id:           the-launcher-is-replaceable
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:scripts
description:  a template with named holes, replaceable wholesale or removable
              entirely.

A script runs inside a generated launcher. That launcher is a template with named holes:

@header @globals @console @before @prefix @import @entry @onSuccess @onError @suffix

--launcher replaces it with source or a file. --no-launcher removes it entirely: the script is handed to the runtime with nothing installed, nothing captured and nothing wrapping the error.

Fills may reference each other, which is what makes rearrangement possible rather than just substitution – a suffix ending in @prefix runs the prefix again on the way out. That also makes cycles possible, so the reference graph is checked before anything is substituted and a loop is reported by naming the path that closed it. Left unchecked it would be a hang, found by waiting.

A placeholder resolving twice is an error unless named in plumbing.launcherPlaceholderRepeat, because the usual cause is a mistake that silently doubles an effect. The entry block is braced so a permitted repeat genuinely runs twice – allowing a repeat and then emitting code that cannot parse would be worse than refusing it.

A misspelled placeholder is caught by near-miss comparison, including transpositions, because @entyr for @entry is a swap and plain edit distance scores that as two changes. Left in place it becomes a syntax error from the runtime pointing at a line the user did not write.

--launcher takes a required value and --no-launcher is separate. An optional-value flag reads better, but Go implements that only by treating the flag as boolean, and then --launcher mine.ts silently runs mine.ts as the script with no launcher at all. That was found by testing rather than reading.

2026-09-28T02:30:00-05:00

Search paths

id:           search-paths
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:config
description:  lists with a splice point, accepting files as well as
              directories.

paths.scripts and paths.config are lists. A null entry stands for the built-in locations, so ["./mine", null] searches yours first and then the usual places without restating them. A list without a null replaces outright, which is what most people mean most of the time.

An entry may name a file rather than a directory. Somebody with one script in an odd place should be able to point at it without inventing a directory to hold it.

A name matching both foo.ts and foo.js is refused. Which one runs was a coin flip, and preferring one quietly means an edit to the other does nothing with no indication why. plumbing.allowTsJsOverlap permits it, and then .ts wins, because a project holding both is almost always compiling one into the other.

When a script is not found, the error prints the path that was actually searched, marking what was missing and what was a file. That is the question being asked.

2026-09-28T02:30:00-05:00

Checking before running

id:           checking-before-running
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:scripts
description:  everything knowable is checked before a server starts.

A malformed --env pair, an unreadable hook, a log directory that is a file – all knowable before any work begins, all previously found later and more expensively. A bad --env pair was silently ignored, so the script ran without the variable and behaved as though it had never been asked for.

Everything is checked in one pass, so a run with three bad paths reports three problems rather than the first and then two more runs. A missing writable directory is created rather than refused, because the intent is unambiguous. A missing optional path is a warning, because refusing to start over an empty script directory would make every fresh checkout noisy.

--typecheck resolves and checks the generated program without running it, which answers “will every import resolve” before the servers are up. Off by default because it costs a second or two on a cold module cache; worth turning on for anything scheduled, where the cost is irrelevant and a broken import at three in the morning is not. Under Node and Bun it reports that it cannot run rather than pretending it did, because those strip types instead of checking them.

2026-09-28T02:30:00-05:00

What the console does and does not capture

id:           what-the-console-does-and-does-not-capture
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:logging
description:  console is redirected, faithfully; raw byte writes are not.

All twenty-five console members are handled. Those carrying a level become that level. console.log still reaches stdout, because stdout is the script’s result. trace carries structured frames, clear resets indentation without erasing a durable log, and the devtools markers become file-only records rather than vanishing.

The test that matters compares a script’s output under capture against the same script run with --no-launcher, line for line. Not “does it log” but “is it the same”. That formulation is the only one that cannot drift, and it caught format specifiers: console.info("%s scored %d", name, n) was rendering as the template beside its arguments instead of substituting them.

Names are preserved, so console.info.name is still "info". Messages use the runtime’s own inspect, so console.info({a:1}) reads { a: 1 } exactly as it would unwrapped – the structured value is already in args, so the message is free to be the human form.

console.createTask() throws when called bare. So does Deno’s. The test compares against the unwrapped console rather than asserting nothing throws, because matching the original includes matching its failures.

Raw writes are not captured. Deno.stdout.write and Deno.stderr.write go straight out. A script reaching for bytes has asked for bytes, and wrapping them in records would be the wrong answer. Use console or log for anything meant to be recorded.

2026-09-28T02:30:00-05:00

Structured stack traces

id:           structured-stack-traces
since:        pre-release
created:      2026-09-28T02:30:00-05:00
last-updated: 2026-09-28T02:30:00-05:00
status:       standard
tags:         area:logging
description:  frames as data, from V8, in all three runtimes.

Error.prepareStackTrace is V8-specific and in no standard, and works in Deno, Node and Bun alike. It hands back call sites instead of a formatted string, so frames arrive as data: function, file, line, column, and the async and native flags. Parsing the string form loses those flags and breaks on any path containing the characters the format uses as delimiters.

One trap is worth recording. V8 memoises whatever prepareStackTrace returned the first time .stack is read. Asking for frames destroys the string; asking for the string destroys the frames. Both are wanted, so frames are captured and the string is rendered from them. Two attempts at this each lost one form before a test asserted both survive.

captureFrames(skip, limit) and errorFrames(err) are available to scripts.

2026-09-28T02:30:00-05:00

Cancellation reaches the transport

id:           cancellation-reaches-the-transport
since:        pre-release
created:      2026-09-28T03:40:00-05:00
last-updated: 2026-09-28T03:40:00-05:00
status:       standard
tags:         area:transport
description:  a call timeout can interrupt a hung HTTP server.

The Streamable HTTP transport detached its POST from the caller’s context. The intent was presumably that a request in flight should finish rather than be abandoned half-processed, which sounds reasonable and was wrong.

Send is synchronous. A server that accepts the connection and never answers blocks inside it, before the caller reaches the select that watches for a timeout. Detaching the request did not make cancellation best-effort, it removed it: pool.callTimeout could not interrupt a hung server at all, and the only bound left was the transport’s own ten-minute ceiling.

The request now carries the caller’s context. Protocol-level cancellation is unaffected, because notifications/cancelled is deliberately sent on its own background context so that it outlives the request it cancels.

The test blocks a server and asserts Send returns. It fails on the previous code after five seconds and passes in under one.

2026-09-28T03:40:00-05:00

Where a setting is read

id:           where-a-setting-is-read
since:        pre-release
created:      2026-09-28T04:30:00-05:00
last-updated: 2026-09-28T04:30:00-05:00
status:       standard
tags:         area:config
description:  what the registry governs, and what still has its own path.

Declaring a setting and validating it is not the same as applying it. A value that is accepted, reported as fine, and then ignored is documentation that lies more quietly than a missing flag does.

These come from the resolved set, and therefore work identically from a file, a variable or a flag:

  • logging.format, logging.level, logging.source, logging.dir
  • catalog.budget, catalog.bias
  • script.typecheck
  • paths.scripts
  • plumbing.allowTsJsOverlap

Pool settings are read by the configuration loader rather than the registry, because they are per-server and the server’s own block has to win. pool and pool are two spellings of one block: pool reads naturally beside mcpServers, pool is what mcpx config --schema calls it. Both exist because having one of them silently do nothing would be worse than having two. Where both appear, pool wins – it is the older spelling, and adding a synonym should not change what an existing file means.

Still on their own paths, and honestly so: output.json (the global --json flag is read before any command), paths.state and paths.cache (needed before settings can be resolved, since they say where to look), and most of daemon.*.

2026-09-28T04:30:00-05:00

Placeholders that take arguments

id:           placeholders-that-take-arguments
since:        pre-release
created:      2026-09-28T07:30:00-05:00
last-updated: 2026-09-28T07:30:00-05:00
status:       standard
tags:         area:scripts
description:  @name(a, b), and @names a user declared.

A placeholder may take arguments:

@console(header, prefix)

An argument naming a placeholder expands to it; anything else is literal. They bind inside the body only, so two uses of one placeholder cannot contaminate each other. Inside, they are addressable by the names the declaration gave them, and always as @1, @2 and @args.

This is the difference between a template that can be rearranged and one that can only be filled in.

A file may declare the @name it provides:

// @mcpx:placeholder timed(body, label)
{
  const __t = performance.now();
  @body
  log.info("timing", { label: @label, ms: Math.round(performance.now() - __t) });
}

Three spellings, because the right one depends on the file. A comment works in any language and cannot affect runtime. An exported MCPX_PLACEHOLDER constant is visible to tooling. The filename is the least ceremony for a directory of one-line fragments.

Shadowing a built-in is refused: a template that reads correctly and means something else is the worst kind of surprise. Two files claiming one name is refused for the reason two config keys are – there is no order between them.

paths.placeholders is empty by default, because scanning every script directory for declarations would make an ordinary script’s filename quietly meaningful.

The single-resolution rule composes with this. If @timed uses @entry and the template also uses @entry directly, that is a repeat and is refused unless named in plumbing.launcherPlaceholderRepeat.

2026-09-28T07:30:00-05:00

The harness knows what the agent does not

id:           the-harness-knows-what-the-agent-does-not
since:        pre-release
created:      2026-09-28T07:30:00-05:00
last-updated: 2026-09-28T07:30:00-05:00
status:       standard
tags:         area:integration
description:  session identity arrives through the environment, not the model.

mcpx leases servers per session. That only works if it can tell sessions apart, and nothing in a shell command carries that: the agent does not know its own session id, and asking it to pass one would spend tokens on plumbing and be forgotten half the time.

So the harness supplies it. plugin/opencode/mcpx-session.ts puts a handful of variables into every shell command; mcpx reads them and the agent never learns any of it happened.

MCPX_TRACE_IDS is a list of pairs rather than a flat id, because a flat one cannot say “this session, whose parent is that one, in this worktree”. An entry longer than a pair names several ids for one key. Unknown keys are kept, since the list exists to grow, and they land on every record as id.<key>.

They cannot overwrite what mcpx established. A caller may add context; it may not rewrite which session a call was actually leased for, or leasing becomes advisory.

Everything past the session id is opt-in. Environment variables are not context – the model never sees them – so anything cheap goes in on the reasoning that an unread variable is cheaper than a missing one. Transcript lengths and token counts stay out: each would be a database query on every shell command, for a number almost nobody reads. Those come from mcpx stats --opencode, which asks once.

2026-09-28T07:30:00-05:00

Statistics from opencode

id:           statistics-from-opencode
since:        pre-release
created:      2026-09-28T07:30:00-05:00
last-updated: 2026-09-28T07:30:00-05:00
status:       standard
tags:         area:inspection
description:  read-only reporting over opencode's own database.

mcpx stats --opencode reports overview, agents, models, projects, busiest sessions and activity. What a run cost, which model answered, how much was a subagent’s – those live in opencode’s database and are pruned over time, so folding them in beside mcpx’s own log means one place to ask.

The database is opened read-only and belongs to a program that may be running. mode=ro rather than immutable=1, deliberately: immutable would permit reading a torn page from a live writer. Every statistic checks for the columns it needs, because that schema is not mcpx’s to depend on – one that has moved should cost a number, not the command.

The model column holds JSON rather than a name, and it is decoded in Go rather than with json_extract, which is present in most SQLite builds and absent in enough of them to be worth avoiding.

2026-09-28T07:30:00-05:00

The explorer

id:           the-explorer
since:        pre-release
created:      2026-09-28T07:30:00-05:00
last-updated: 2026-09-28T07:30:00-05:00
status:       standard
tags:         area:ux
description:  a prompt loop over the discovery commands, for a human.

Everything mcpx explore shows is available elsewhere. It exists because discovery is a loop – list namespaces, look at one, read a signature, try it, see what the log said – and running four commands with different flags to go round it once is enough friction that people stop and guess instead.

Deliberately not full-screen. A pane interface needs a terminal library, raw mode, resize handling and a redraw loop, and loses the two things a plain prompt gives for free: output stays in scrollback where it can be copied, and every screen corresponds to a command that can be scripted. The footer prints that command, so the tool teaches its own non-interactive form rather than being a place where knowledge stops.

A bare word naming a namespace shows its signatures, because that is what somebody means nine times in ten. chain with no argument lists recent traces, because needing another command to find this one’s argument is exactly the friction being removed.

The terminal check asks the kernel for terminal attributes rather than testing ModeCharDevice. That test is wrong in the direction that matters: /dev/null is a character device, so redirecting stdin from it read as interactive and blocked on a prompt nobody was there to answer.

2026-09-28T07:30:00-05:00

The browser

id:           the-browser
since:        pre-release
created:      2026-09-28T09:00:00-05:00
last-updated: 2026-09-28T09:00:00-05:00
status:       standard
tags:         area:ux
description:  three panes, so comparing two tools is a keystroke.

mcpx tui, mcpx --tui, or just mcpx when there is a terminal to draw on.

Three panes – namespaces, the selected namespace’s tools, one signature – so the question “which of these two tools do I want” is a cursor move rather than two commands and a scrollback hunt. Moving the cursor loads the next pane; nothing needs pressing.

L switches to the log. r refreshes. / filters either list.

Why both this and the prompt

mcpx explore and mcpx tui answer differently shaped questions. The prompt is better when you know what you want and will paste the result somewhere: its output stays in scrollback and every screen names the command that produced it. The browser is better when you do not know yet, because three panes hold more context than a scrolling transcript.

The prompt also works where the browser cannot run – over a pipe, in CI, inside another program – so neither is a worse version of the other.

Six views

] and [ move between them; the header names them all with the current one marked, because a view nobody knows exists is a view nobody uses.

  • tools – namespaces, their tools, one signature
  • log – every record, newest last
  • stats – calls, servers, errors, sessions, slowest, volume; d cycles
  • servers – individual processes, with pid, uptime and why each stopped
  • sessions – what mcpx saw merged with what opencode recorded
  • storage – what all of it costs on disk

Five of the six are grids, rendered through one table type. A new view is therefore a query rather than a widget, and every view gets selection, truncation and drill-down without restating any of it.

sessions is the one that needed both halves. mcpx knows which servers a session used; opencode knows what it cost. Neither alone is the row anybody wants, and a machine without opencode still gets mcpx’s half rather than an error.

storage exists because it answers a question that arrives suddenly – something is large and it is not obvious what – and answering it otherwise means knowing where four different things live.

Opening a row

Enter, or a click. A log line is truncated to fit a terminal, and the part cut off is usually the part being looked for: a stack, a full path, a nested result. Expanding it is the difference between the log being browsable and being a place to notice that something exists before going to another command to read it.

Multi-line strings are indented rather than escaped, since escaping them is what made them unreadable in the row.

The dependency

bubbletea, bubbles and lipgloss. They cost 1.2 MB in the stripped binary, taking it from 11.2 to 12.4 MB, and replace what would otherwise be hand written escape sequences, resize handling and a redraw loop. That is code which is never finished and never correct on every terminal.

Mouse support is on, in cell-motion mode: clicks and the wheel, without an event per pixel of movement.

Capturing the mouse does take over text selection. Every terminal worth using restores it on shift-drag, which is the convention, and being unable to click a log line open is a worse trade than learning one modifier.

Testing a full-screen program

The view reads from an interface, not from the daemon client, so a fake drives it in tests. A terminal program that cannot be tested is one that breaks quietly, and the failures worth catching are not visual: that moving the cursor loads the right namespace, that a late reply for a namespace the cursor has already left is dropped rather than shown, and that keys go to the filter while it is open – without that last one, typing a namespace containing “q” quits.

2026-09-28T09:00:00-05:00

mcpx speaks MCP

id:           mcpx-speaks-mcp
since:        pre-release
created:      2026-09-28T11:00:00-05:00
last-updated: 2026-09-28T11:00:00-05:00
status:       standard
tags:         area:integration
description:  the inversion: mcpx as a server, not only a client.

mcpx exists so tool schemas never reach a model’s context. A host that speaks only MCP – a different editor, a hosted agent, anything that is not opencode – could not use any of that.

So mcpx serve offers mcpx itself as an MCP server: ten tools reaching every server it knows about, with the schemas still on this side of the wire.

The surface is small on purpose. Exposing three hundred tools over MCP would rebuild the problem mcpx was built to solve, with extra steps. mcpx_exec is the one that matters: it runs TypeScript next to the servers and only what it prints comes back.

stdio and Streamable HTTP, plus a plain POST per tool at /v1/tools/<name>. Offering only JSON-RPC would make mcpx reachable from MCP hosts and from nothing else, which is the opposite of the point – a shell script with curl should ask the same questions an agent does.

2026-09-28T11:00:00-05:00

Command-line programs as servers

id:           command-line-programs-as-servers
since:        pre-release
created:      2026-09-28T11:00:00-05:00
last-updated: 2026-09-28T11:00:00-05:00
status:       standard
tags:         area:integration
description:  a declaration, not a wrapper process.

An enormous amount of capability already exists as command-line programs. Writing an MCP server to wrap one is a day’s work producing a process whose only job is to shell out, and there are hundreds of such programs.

paths.adapters points at declarations instead:

{ "adapters": [{
  "name": "gitx", "command": "git",
  "tools": [
    { "name": "log", "args": ["log", "--oneline"],
      "params": [{ "name": "limit", "flag": "-n", "type": "integer", "default": "10" }] }
  ] }] }

That is git as an MCP server. It appears in tools/list, it is callable from a script as await gitx.log({ limit: 5 }), and mcpx adapter check says whether the binary is even installed. The daemon runs each adapter as a server of its own – mcpx adapter serve gitx, spawned like any upstream – so it is a namespace in mcpx ls, types, search, catalog, /v1/call and the generated client, with a signature typed from the declared params. An adapter cannot share a name with a configured server. internal/e2e/toolparity_test.go holds every tool in tools/list to being in the generated client, and calls one from a script.

Not a shell escape. A tool is a named subcommand with declared parameters, so a model cannot invent a command line and what is reachable is exactly what somebody wrote down.

Two details that are wrong in the obvious implementation. A boolean parameter becomes the flag’s presence, because --verbose true is wrong for almost every program ever written. And a non-zero exit is a result rather than an error: the program ran and said something, and deciding that is a failure belongs to whoever asked.

2026-09-28T11:00:00-05:00

Finding servers that are not configured yet

id:           finding-servers-that-are-not-configured-yet
since:        pre-release
created:      2026-09-28T11:00:00-05:00
last-updated: 2026-09-28T11:00:00-05:00
status:       standard
tags:         area:integration
description:  the official registry, and why not HAPI.

mcpx registry search weather asks a registry what exists. mcpx registry add <name> --write puts it in the configuration file.

Which registry

The official MCP Registry, at registry.modelcontextprotocol.io. It is unauthenticated, returns real data, and – the part that matters – publishes an OpenAPI specification that other registries implement. Writing against the specification rather than against one host means the same code reaches the official registry, a vendor’s subregistry, and whatever an organisation runs internally to control what its agents may install. registry.url points it anywhere.

HAPI was the other candidate and is not usable: the framework is not open source and its public API returns 404.

Translating an entry

A registry entry says what a server is, not how this machine should run it.

Remotes are preferred when offered, because nothing is installed and nothing runs locally. Otherwise the package becomes an ephemeral runner – npx -y, uvx, docker run --rm – since a server pinned in a config file should not also require the machine to have been prepared. A publisher’s own runtime hint wins over that table, because they know something about their package that a table does not.

npm versions are pinned. A configuration that silently upgrades is one that breaks on a morning nobody changed anything.

Secrets are never invented. The registry says which variables a server requires; those are reported so the caller can set them, because a server that exits immediately for a missing key looks broken rather than unconfigured.

The reverse-DNS name is stripped to its last component, so io.github.microsoft/playwright-mcp becomes playwright_mcp rather than making every call read io_github_microsoft_playwright_mcp_navigate.

2026-09-28T11:00:00-05:00

Specifications as tools

id:           specifications-as-tools
since:        pre-release
created:      2026-09-28T13:00:00-05:00
last-updated: 2026-09-28T13:00:00-05:00
status:       standard
tags:         area:integration
description:  OpenAPI documents become callable tools.
mcpx api tools --spec https://petstore3.swagger.io/api/v3/openapi.json
mcpx api call petstore_findpetsbystatus '{"status":"available"}' --spec ...

An enormous amount of capability is already described by a specification somebody else maintains. A specification is a better source than a hand-written wrapper for the same reason a generated client is better than a hand-written one: it is already correct, already complete, and it changes when the service does.

paths.apis declares them permanently, and the operations appear over MCP beside everything else.

Two things easy to get wrong

Relative server URLs. The Swagger petstore declares /api/v3, which must resolve against wherever the document was fetched. Unresolved it produces a request with no scheme, and the failure reads as a network problem rather than an unresolved reference.

Only reads, by default. A specification describes what a service can do, not what you meant to allow. --methods all exposes the rest; the difference between listing orders and cancelling them should be a deliberate keystroke.

Deprecated operations are skipped unless asked for, and labelled when included. Path parameters are escaped, because a slash in one silently changes which endpoint is called.

2026-09-28T13:00:00-05:00

Finding a tool without leaving the script

id:           finding-a-tool-without-leaving-the-script
since:        pre-release
created:      2026-09-28T13:00:00-05:00
last-updated: 2026-09-28T13:00:00-05:00
status:       standard
tags:         area:scripts
description:  search() and describe(), inside the program.
for (const hit of search("grep")) {
  console.log(hit.namespace + "." + hit.tool, hit.required);
}
console.log(describe("fff_nix.grep"));

Without these, discovering a tool means ending the script, running mcpx search, reading the result and writing a new script. That round trip is the expensive part: for a model it is a whole turn, and the intermediate result passes through its context on the way.

Synchronous, because everything they search is already in the generated client. A promise would only be a promise of work already done.

A word matching a tool’s name scores higher than one matching its prose, so a tool called grep beats one whose description merely mentions grepping.

2026-09-28T13:00:00-05:00

The half of MCP that is not tools

id:           the-half-of-mcp-that-is-not-tools
since:        pre-release
created:      2026-09-28T15:00:00-05:00
last-updated: 2026-09-28T15:00:00-05:00
status:       standard
tags:         area:protocol
description:  prompts and resources, which mcpx reported as empty.

A server publishes three things: tools, resources and prompts. mcpx handled tools, read resources only when asked by URI, and reported no prompts at all – while its MCP server declared the capability and returned an empty list.

That last part is the worst kind of wrong, because a client cannot detect it. It asks once, receives nothing, and never asks again.

mcpx prompts                              # what servers publish
mcpx prompts demo.summarise text="..."    # render one
mcpx resources                            # what exists
mcpx resources demo/demo://greeting       # read one

Prompts are a server saying “here is the wording that works for this” rather than “here is a function”. A server publishing a good one has encoded expertise that would otherwise be rediscovered by whoever writes the request.

Both pass through the MCP server, namespaced – two servers may publish the same URI, and a caller otherwise has no way to say which it meant.

A server that does not support prompts answers method-not-found. That is an absence, not a failure, so it is treated as an empty list; the alternative makes every listing fail on the majority of servers.

Binary resources are described rather than inlined. A megabyte of base64 in a model’s context is the failure this whole tool exists to prevent.

2026-09-28T15:00:00-05:00

mcpx doctor

id:           mcpx-doctor
since:        pre-release
created:      2026-09-28T15:00:00-05:00
last-updated: 2026-09-28T15:00:00-05:00
status:       standard
tags:         area:ux
description:  one command for "it does not work".

mcpx has a daemon, a runtime, a configuration chain, a log index, adapters, registries, API specifications and a plugin. “It does not work” stopped being a question with one answer some time ago.

mcpx doctor checks each in the order somebody would have to know to check them by hand: the runtime, git, whether the settings contradict each other, whether the configuration parses, whether every server’s command is actually installed, the directories, the daemon, the log index, and the optional integrations.

That server-command check is the important one. It is the most common cause of “mcpx does not work” and it is invisible until something tries to call the server, at which point the error arrives from three layers down.

Every check says what to do about it. One that only reports a problem leaves the reader exactly where they started.

It found a bug immediately

The unknown-key warning was reporting mcpServers, logging, paths and every server name as unrecognised. They are not: a section is the path to a setting, and mcpServers is the document’s own content. The warning listed everything, which made a real typo invisible among the noise – so it had been useless since the day it was written, and nothing displayed it until now.

2026-09-28T15:00:00-05:00

Types, in whichever form a consumer reads

id:           types-in-whichever-form-a-consumer-reads
since:        pre-release
created:      2026-09-28T16:30:00-05:00
last-updated: 2026-09-28T16:30:00-05:00
status:       standard
tags:         area:integration
description:  four renderings of one fact.
mcpx schema --format typescript    # what a script imports
mcpx schema --format json-schema   # what a validator reads
mcpx schema --format openapi       # what a client generator reads
mcpx schema --format mcp           # what an MCP host reads

The same information every time. A caller should not have to reshape one into another, and each of those four consumers is real.

JSON Schema goes under $defs, which is where a reader looks for named subschemas, so the result is itself a valid schema document rather than a bag of them.

The specification described the wrong thing

mcpx openapi described mcpx’s own ten tools and nothing else. The three hundred it actually fronts were reachable from MCP, and from a script, and from nowhere a generated client could see.

A specification that describes the wrapper but not what it wraps is a specification of the wrong thing. Every upstream tool now has a path:

POST /v1/call/<namespace>/<tool>

Served as well as described – curl -X POST .../v1/call/fff/grep -d '{"query":"x"}' reaches the same server a script does. The document is generated per request rather than at startup, so a server that appears later is described without a restart.

2026-09-28T16:30:00-05:00

Two protocol eras

id:           two-protocol-eras
since:        pre-release
created:      2026-09-28T20:00:00-05:00
last-updated: 2026-09-28T20:00:00-05:00
status:       standard
tags:         area:protocol
description:  legacy and modern, both directions.

The specification splits implementations in two. Legacy revisions (2025-11-25 and earlier) negotiate once through an initialize handshake. 2026-07-28, the current one, carries the version in _meta on every request, has no handshake at all, and requires server/discover.

The matrix is unforgiving: modern against legacy fails, legacy against modern fails, only a dual-era implementation bridges. mcpx is dual-era on both sides.

As a client it probes and falls back. Legacy first by default, because nearly every server in existence is legacy and probing modern first costs a wasted round trip on all of them. That is correct today and will stop being correct, which is why PreferModern, ForceLegacy and ForceModern exist. A recognised UnsupportedProtocolVersionError stops the fallback: it identifies a modern server, so the version is wrong rather than the era, and falling back would report the wrong problem.

As a server it answers both, and no longer lies. negotiate() used to echo whatever version was asked for, so a client requesting 2026-07-28 was told yes and then found no server/discover. It now answers UnsupportedProtocolVersionError (-32022) with the list that would work, and refuses a modern version over initialize at all – a client sending initialize is legacy by definition, so agreeing would promise a protocol neither side is speaking.

2026-09-28T20:00:00-05:00

When a server asks a question

id:           when-a-server-asks-a-question
since:        pre-release
created:      2026-09-28T20:00:00-05:00
last-updated: 2026-09-28T20:00:00-05:00
status:       standard
tags:         area:protocol
description:  elicitation, stored rather than blocked on.

Every other MCP method runs client to server. Elicitation runs the other way: a server stops mid-call and asks something – which repository, are you sure, log in here.

The bug that came first

recvLoop matched inbound frames on their id alone. A server-initiated request has an id and a method, so it looked like a reply to nothing and was discarded. The server waited, the call hit its timeout, and mcpx reported a timeout: true, useless, and pointing at the wrong thing.

Frames are now routed by shape, and a question is always answered – with cancel when nobody was asked, because silence is indistinguishable from a hung server while cancel is honest about nobody having chosen.

A question is state, not a blocked call

That is the whole design. A question gets an identity, a deadline and a row in the database beside the log index. The call stops and says “I need input, here is the ticket”.

So whoever answers need not be whoever asked. CI raises a question a person answers from a laptop twenty minutes later; the reattach works because nothing was ever held in memory. A call that is waiting exits 75 (EX_TEMPFAIL), which is what it is: not a failure, a “try again when you have an answer”.

Expiry answers cancel, never decline. Expiry means dismissed without choosing; telling a server the user declined would say something different and untrue. Overdue questions expire when something reads them rather than on a timer, so there is no sweeper to go wrong and nothing is reported as pending when it is not.

Who answers

The specification leaves this to the client, deliberately: “If the client is an agent, it might decide how to handle the elicitation.”

The default is the agent, and that is usually right – it asked for the thing, the question is part of that request, and it has the context. A human is pulled in only for what an agent cannot know or should not hold: a credential, a browser flow, or a bare confirmation, since consent is not the agent’s to give.

Every routing decision records its reason. Routing nobody can inspect is routing nobody can correct.

Answering

mcpx elicit list
mcpx elicit answer elc-9f2c1a84 repo=me/thing    # key=value, or JSON
mcpx elicit decline elc-9f2c1a84

key=value is accepted because most answers are one short string, and making somebody quote JSON for that is ceremony. mcpx elicit show prints the command that answers a question, because the alternative is assembling it from three fields and getting it wrong once.

2026-09-28T20:00:00-05:00

Credentials

id:           credentials
since:        pre-release
created:      2026-09-28T20:00:00-05:00
last-updated: 2026-09-28T20:00:00-05:00
status:       standard
tags:         area:config
description:  what the specification actually requires, which is little.

The specification’s position is short and the opposite of what people assume:

  • Authorization is optional.
  • stdio transports SHOULD NOT use OAuth. They take credentials from the environment – a child process is trusted because you started it.
  • HTTP transports SHOULD use OAuth 2.1 when the server is protected.

So most servers need nothing.

{ "mcpServers": { "api": {
    "url": "https://example.com/mcp",
    "auth": { "type": "bearer", "token": "${API_TOKEN}" } } } }

bearer, basic, header, query and env, all with ${VAR} expansion so the secret is never in the file. An unset variable is named up front rather than surfacing as a 401 three layers away.

Describe() prints a reference but never a literal, because its output is what people paste into issues.

oauth is declared but not performed. A server requiring it says so before the first request rather than failing with a 401 nobody can interpret.

2026-09-28T20:00:00-05:00

The rest of the protocol

id:           the-rest-of-the-protocol
since:        pre-release
created:      2026-09-28T22:00:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:protocol
description:  the features that are not tools, and which of them mcpx has.

Tools, resources and prompts are the famous three. The specification defines several more, and mcpx was silently dropping most of them.

Now implemented

Roots. A client tells a server which directories it may work within. Without it a filesystem server has to be told through its own configuration, separately, in a second place that drifts from the first.

Server log messages. notifications/message carries a server explaining itself – “retrying against the replica” tells you exactly why a call was slow. Every one was discarded. Note that servers send none until asked, via logging/setLevel, so a client that never asks concludes servers do not emit any.

Progress. notifications/progress on a long call, previously dropped.

List-changed notifications. A server saying its tools have changed means the cached schema is stale. mcpx already computes catalog diffs; it just was not listening for the event that should trigger one.

Pagination when serving. mcpx fronts every tool of every configured server, and a client with a frame limit had no way to read that list. The cursor is opaque – base64 of an offset – because the specification says so and a client that parses one is relying on something it was told not to. An invalid cursor starts from the beginning rather than failing: a client cannot validate an opaque value before sending it, so refusing would strand one that has nothing better to send. That is a deliberate departure from a SHOULD – the specification asks for -32602, and docs/spec/server-obligations.md §4.3 records the obligation being departed from. #257 is where it gets revisited.

Completion. completion/complete answers from what mcpx already holds. A client that offers autocomplete and receives method-not-found shows nothing, and the user concludes the feature is broken rather than absent.

Cancellation, inbound. Recorded rather than dropped. mcpx cannot yet interrupt an in-flight upstream call – that needs the request id plumbed through the pool – but a cancellation silently discarded leaves a client unable to tell whether the message arrived.

Sampling. Shipped since this entry was written; see Sampling below for what it does and the ordering bug that made it unreachable at first.

Still not implemented, and declared as such

Capabilities are declared only where mcpx can actually deliver. Claiming one it cannot serve invites a server to use it and get silence, which is worse than not offering it at all. What mcpx declares, per revision and per direction, is docs/protocol.md §2.2; what the specification would require of a server that declared more is docs/spec/server-obligations.md.

2026-09-30T14:30:00-05:00

One daemon, many clients

id:           one-daemon-many-clients
since:        pre-release
created:      2026-09-28T23:30:00-05:00
last-updated: 2026-09-28T23:30:00-05:00
status:       standard
tags:         area:integration
description:  a socket, a port, or a machine somewhere else.

The daemon has always served its whole API over HTTP. Two things stopped that being useful beyond one machine: the client hardcoded a unix-socket dialler, and the listener hardcoded loopback. Both were one line.

MCPX_DAEMON_ENDPOINT=http://mcpx.internal:8899    # a daemon on a VPN
MCPX_DAEMON_ENDPOINT=unix:///run/mcpx/other.sock  # a different local socket
mcpx daemon --address 0.0.0.0 --port 8899         # serve a network

Works for the CLI, the plugin, and generated scripts – which could already do it, since they were given MCPX_ENDPOINT from the start.

A configured endpoint also means mcpx will not try to start that daemon. Falling back to a local one when a remote is unreachable would silently answer from the wrong machine, which is worse than failing.

Binding beyond loopback prints a warning, once, because the API is unauthenticated: whatever can route to the port can run tools as you. That should be a sentence somebody read rather than a default they inherited.

What each transport costs

Same request, mean of thirty:

spawn mcpx status23.12 ms
unix socket, new connection0.27 ms
unix socket, keep-alive0.17 ms
tcp loopback, new connection1.59 ms
tcp loopback, keep-alive0.65 ms

Spawning is 135× slower than a warm socket, and almost all of it is process startup rather than transport. Irrelevant for something run once; decisive for anything on the path of every tool call.

plugin/opencode/mcpx/daemon.ts therefore talks to the socket directly rather than shelling out. It returns undefined when no daemon is reachable rather than throwing, because a plugin that fails to load has broken the editor for a tool the user may not even be using.

It sat unused for a day: written, documented, benchmarked, and imported by nothing. The plugin still spawned mcpx log record after every tool call. Wiring it in found three more things, each invisible until something real used the path:

  • The socket was guessed. The client took the newest .sock in the state directory. Sockets are keyed by configuration, so that is the wrong daemon whenever two exist, and none at all when a long state path moved the socket elsewhere. It now asks mcpx --json status, once.
  • status said running only when false. Testing it read a live daemon as down.
  • One file, two daemons. The key hashed the configuration’s path as spelled. /tmp is a symlink to /private/tmp on macOS, so a process whose $PWD held the short form computed a different key, saw no daemon, and started a second. Paths are now resolved through symlinks first.

With those fixed, a timing record costs 0.09 ms over the socket instead of a 23 ms spawn.

2026-09-28T23:30:00-05:00

Hearing what the daemon is doing

id:           hearing-what-the-daemon-is-doing
since:        pre-release
created:      2026-09-29T11:00:00-05:00
last-updated: 2026-09-29T11:00:00-05:00
status:       standard
tags:         area:integration
description:  one event bus, two views of it.

MCP’s own subscription mechanism, subscriptions/listen, carries four kinds of notification: tools, prompts and resources changing, and a subscribed resource updating. That is the right set for a server telling a client its catalogue moved, and nowhere near enough for what a client of mcpx wants to hear – a question waiting for an answer, a server that crashed, a call that finished.

So there is one bus and two views of it:

carriesfor
subscriptions/listenthe four the specification definesany MCP client
GET /v1/eventseverything mcpx noticesthe plugin, the TUI, a script, curl

Both read the same stream, so they cannot disagree about what happened. The MCP view is deliberately narrow: the specification says a server MUST NOT send a notification type the client did not request, so nothing mcpx-specific leaks into it.

Server-sent events, not WebSockets

SSE is plain HTTP. It goes through every proxy HTTP does, a browser’s EventSource reconnects it without any code knowing, and consuming it takes a GET. WebSockets would buy bidirectionality nothing here needs – answers go back as ordinary POSTs – at the cost of an upgrade that half of all middleboxes mishandle.

Lossless reconnection

Every event carries a sequence number. Reconnect with Last-Event-ID (which EventSource sends for you) or ?since=N and everything after N is replayed.

?since=0 replays everything retained; no position means live only. Those are different requests, and conflating them – which an early version did – made “replay from the start” silently return nothing, indistinguishable from nothing having happened.

A reconnect older than the retained history gets an in-band gap event, so the subscriber can resynchronise rather than trust a stream with a hole in it.

Publishing never blocks. One slow reader cannot stall the daemon; it misses events and has a counter to say so.

2026-09-29T11:00:00-05:00

Sampling

id:           sampling
since:        pre-release
created:      2026-09-29T11:00:00-05:00
last-updated: 2026-09-29T11:00:00-05:00
status:       standard
tags:         area:protocol
description:  a server asking for a model's help, passed to whoever has one.

Sampling is part of the MCP specification, in every revision – not an opencode feature. It lets a server ask its client for a model completion: “summarise this”, “classify that”, “draft a reply”. The server gets an intelligence it does not have to host or pay for.

It is the mirror of elicitation:

asksfor
elicitationa person or agenta value – which repository, are you sure
samplinga modeltext – a completion the server will use

mcpx has no model, so it cannot answer either itself. What it can do is carry the request to something that can. A sampling request becomes a stored question – same table, same deadline, same routing as elicitation – and whatever drives mcpx answers it. The opencode plugin is the natural answerer: it holds a session with a real model already.

Declining a sampling request becomes an error, because the specification has no decline result for it. A result missing role or model has them filled, since strict servers reject one without them for a reason unrelated to its content.

2026-09-29T11:00:00-05:00

It did not work, and nothing said so

The passthrough above shipped unreachable. A server only sends a sampling request to a client that declared sampling, and mcpx never did: capabilities are declared in the handshake, and the handler that made sampling possible was installed on the connection after it. Every test of the passthrough called it directly, so every test passed.

The same ordering broke roots. Once the daemon’s handler was installed – which is always – it received every server request, roots/list included, and knew only elicitation and sampling. Roots were configured, and every server that asked for them was told “not implemented”.

Both are fixed by giving the connection its handler and roots at construction, and by answering roots before consulting the handler at all.

2026-09-29T15:00:00-05:00

What 2026-07-28 actually asks of a client

id:           what-2026-07-28-actually-asks-of-a-client
since:        pre-release
created:      2026-09-29T15:00:00-05:00
last-updated: 2026-09-29T15:00:00-05:00
status:       standard
tags:         area:protocol
description:  three MUSTs the first modern implementation missed.

The first modern-era client probed with server/discover, chose a version, and then spoke exactly as a legacy client would. Reading the schema rather than the prose found three requirements it did not meet:

requirementwasnow
_meta carries protocolVersion and clientCapabilities on every request – a server MUST NOT infer capabilities from an earlier onesent on none, not even server/discoverstamped on every modern request, with clientInfo
the HTTP MCP-Protocol-Version header MUST match that _meta version, or 400a constant: the legacy versionread from the frame being sent
every result MUST carry resultTypemcpx’s server sent none"complete" on every reply to a modern request

And one mechanism it did not know existed. A modern server cannot send the client a request of its own – there is no connection to send it on – so it elicits, samples and asks for roots by answering: resultType: "input_required", a map of inputRequests, an opaque requestState. The client answers each, and sends the original request again with inputResponses and the state, exactly as received. mcpx returned the “not yet” as though it were the result, so against a modern server, elicitation, sampling and roots silently did nothing.

Both eras now answer through one function, so a question is answered the same way whether it arrived on the wire or inside a result. The retry loop is bounded (elicit.inputRounds, 8): a server that never stops asking is broken or adversarial, and without a bound the client would answer it forever.

Still missing, and honest about it: mcpx’s server does not yet turn an upstream question into input_required for a modern client of its own. It answers through the broker instead, which works, but a modern client cannot answer inline.

2026-09-29T15:00:00-05:00

Running without a daemon

id:           running-without-a-daemon
since:        pre-release
created:      2026-09-29T11:00:00-05:00
last-updated: 2026-09-29T11:00:00-05:00
status:       standard
tags:         area:integration
description:  the last rung, and what it costs.

Every command walks the same ladder, cheapest first:

1. socket    an existing local daemon          0.17 ms
2. url       a named remote one                0.65 ms
3. spawn     start a daemon, then use it       ~23 ms, once
4. inline    run the servers in this process   every time

A named endpoint stops the ladder at step 2: falling back to a local daemon would answer from the wrong machine.

Inline hosts the daemon’s own API inside the command, on a private socket, for as long as the command runs. It is the identical handler, so it cannot drift from daemon mode.

What changes is lifetime, and it changes completely:

with a daemoninline
a server startsonce, reusedevery command
between commandskept warmgone
a browser sessionsurvives across commandsdies with the command
schema cachesharedread from disk each time

That is why inline is the last rung and off by default. It is for a sandbox with no fork, a read-only filesystem, or a container whose init will not reap – places where the alternative is not working at all. A pool that silently stops pooling is a performance bug nobody can see, so it has to be asked for.

2026-09-29T11:00:00-05:00

Tasks

id:           tasks
since:        pre-release
created:      2026-09-29T11:00:00-05:00
last-updated: 2026-09-29T11:00:00-05:00
status:       standard
tags:         area:protocol
description:  a slow tool call as a handle rather than a held request.

Add task to a tools/call and the reply is a handle, immediately. Poll it with tasks/get, wait on it with tasks/result, stop it with tasks/cancel.

For a genuinely slow tool – a build, a crawl, a browser session – holding a request open for minutes invites every proxy, load balancer and client timeout in between to cut it off. A handle does not.

Core in 2025-11-25, an extension in 2026-07-28, so mcpx declares it both ways and a client of either era finds it where it looks.

Every task has a TTL. The specification allows an unbounded one; mcpx never offers it, because a result nobody collects is memory nobody frees.

2026-09-29T11:00:00-05:00

Nix packaging

id:           nix-packaging
since:        pre-release
created:      2026-09-27T04:20:00-05:00
last-updated: 2026-09-30T16:10:00-05:00
status:       standard
tags:         area:packaging, platform:nix
description:  buildGoModule with a pinned vendorHash; unit tests run in the
              sandbox; deno, bun and node are pinned on the wrapper's PATH.

mcpx has five direct third-party Go dependencies. modernc.org/sqlite backs the log index, and it is a pure-Go translation rather than the usual cgo driver, because a cgo driver would make this derivation need a C toolchain and would break cross-compilation, for a database that is only ever an index over files that remain the source of truth. The other four are one decision each: bubbletea, bubbles and lipgloss for the browser, and gopkg.in/yaml.v3 for OpenAPI documents. Each is argued in package.nix and listed with its reason in docs/dependencies.md.

This entry and docs/dependencies.md both said “one” until the browser and the OpenAPI work landed, while the browser entry above already described three of the four by name — the file disagreed with itself, which is how a count that nothing checks decays.

That reasoning was written down and not enforced: buildGoModule leaves cgo enabled on a native build, so the derivation was free to link against a C toolchain the comment said it must not need. CGO_ENABLED=0 is now set in the derivation itself. It surfaced on a machine with a symlinked /nix, where the cc-wrapper’s purity check trips on gcc canonicalising the path – a build that should never have consulted a C compiler failing because of one.

The wrapper suffixes git, deno, bun-bin and nodejs onto PATH, so script execution does not depend on the calling shell and the two repository layouts native discovery cannot read still resolve. git is a suffix, not a prefix, and since #223 nothing on the default path needs it – see Finding the repository without git.

The man page and the shell completions are generated by the binary the build just produced, so they describe the commands and settings this build actually has. A man page maintained separately is wrong within two releases.

nix build .#mcpx runs the unit suites in the sandbox. The end-to-end suite spawns JavaScript runtimes and binds unix sockets, so it runs outside with go test ./.... The sandbox list is hand-written (package.nix), and of the 32 packages under internal/ that have tests it names 18 — internal/e2e is excluded on purpose, and the other fourteen, internal/api, internal/cli and internal/mcpclient among them, by drift. A list maintained beside a generated one, which is the failure mode this document keeps finding (#285).

2026-09-30T16:10:00-05:00

The comparison register

id:           the-comparison-register
since:        pre-release
created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:compare, area:protocol
description:  511 rows, one per difference between mcpx and the five MCP
              revisions, opencode v1 and v2, lootbox and Cloudflare code mode.

mcpx sits between things that disagree: five revisions of the protocol, two of them eras apart; two opencodes with different plugin APIs; three other ways to run code against MCP tools. Every argument about what mcpx should do next was being had from memory.

docs/compare/ is the register that replaced the memory. One row per difference – one field, one method, one error code, one header, one behaviour – with where it exists, what mcpx did at commit 05c78b2, what adopting it is worth, what it would cost, and a citation for every claim. 511 rows; 171 of them labelled mcpx missing.

The point of a register is that it is specific enough to be wrong. A prose comparison says “mcpx has partial 2026 support”; a register says server/discover answers protocolVersions where the schema says supportedVersions, at internal/mcpserver/server.go:534, against schema/2026-07-28/schema.ts:678. The second can be checked, and checking it produced twenty-four issues (#199 through #220 and their siblings), each grouping the rows one fix would close. Several were fixed in the same week the register was written.

It is dated on purpose. The status column is mcpx @ 05c78b2 and says so in every table header, because a status column with no commit behind it is a column nobody can falsify. Twenty-seven pull requests have landed since; the README lists them and says the column is stale rather than pretending otherwise.

2026-09-30T14:30:00-05:00

Finding the repository without git

id:           finding-the-repository-without-git
since:        pre-release
created:      2026-09-30T07:30:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:config, area:packaging
description:  the repo and worktree scopes read .git themselves, so git is no
              longer a runtime dependency and the image lost 105 MB.

A server scoped repo gets one process per clone; one scoped worktree gets one per checkout. Both keys used to come from forking git twice per directory – rev-parse --git-common-dir and --show-toplevel – and caching the answers, negative ones included, for the daemon’s lifetime.

That cost four things, and three of them were only found by building the replacement:

  • 105 MB of the container image. Debian’s git depends on perl outright.
  • It did not work in the container anyway. A checkout mounted into the image is owned by the host’s uid and the image runs as uid 1000, so git refuses it: fatal: detected dubious ownership in repository at '/work/proj'. git was in the image so that these scopes would work, and in the ordinary way of using the image they silently fell back to per-directory keys.
  • The daemon’s environment leaked into every caller’s key. An autostarted daemon inherits the environment of whoever ran mcpx first. A git hook exports GIT_DIR; a hook that runs mcpx leaves a daemon carrying it for hours, and every later caller from any directory was then keyed to that one repository.
  • The cache made the first answer permanent. A directory that became a repository after the first call there stayed “not a repository” until the daemon exited.

mcpx now reads .git itself – a port of git’s own discovery rules, written against them and recorded in docs/git-discovery.md layout by layout. git is still consulted, once, for the two layouts the native reader deliberately does not implement, and if it is absent those layouts degrade to cwd with a log line and a doctor warning saying which layout and why. That is the whole of the dependency: no fetching, no writing, no network.

The environment leak is pinned by TestTheDaemonsOwnGitDirDoesNotPinEveryCaller, which fails against the old code. The image went from 413 MB to 288 MB.

doctor keeps its git row. An optional dependency whose absence changes behaviour is exactly the thing a diagnostic should report – dropping the row because git became optional would have hidden the one case where its absence still matters.

2026-09-30T14:30:00-05:00

Measured against the official suite

id:           measured-against-the-official-suite
since:        pre-release
created:      2026-09-30T09:00:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:protocol, area:conformance
description:  every frame validated against the real schema, and the official
              MCP conformance suite run against a live daemon in both
              directions.

mcpx claims five revisions in each direction. Until this, all five claims were tested against mcpx’s idea of them.

Two things changed that. First, internal/mcpspec validates real traffic against the official schemas for all five revisions, in strict mode when mcpx is the sender: a key some other revision defines is a failure, because “send conservatively” is precisely the rule a lenient check cannot see. That is what makes a passing test mean something – the previous tests pinned the wrong server/discover field name on both sides at once, which is how it passed CI for weeks.

Second, there is now an official suite: modelcontextprotocol/conformance. It is not optional folklore. SEP-2484, Final, makes a merged conformance scenario a condition of a Standards Track SEP reaching Final, and SEP-1730 ties SDK tiering to a score from it. Its requirements/<revision>.yaml files are frozen, so an implementation is measured against the suite as it stood when it was expected to conform. scripts/conformance.sh runs both legs: mcpx as a server at the daemon’s /mcp, and – through internal/conformance/officialclient – mcpx as the client under test.

It found real defects, including two that no amount of reading found: a modern request missing _meta was answered -32020 when the specification and mcpx’s own doc comment both say -32602, and the five methods 2026-07-28 removed were still being answered to 2026-07-28 peers. The second is the limit of “accept liberally”: offering a method a revision never had withholds nothing, but answering one it removed contradicts the specification naming the replacement.

The numbers – passed and failed per requirement set, the commit they were measured at, and which failures are defects – live in exactly one place, docs/spec/official-suite.md, so they are not repeated here to go stale.

Read the failure counts carefully; most are not defects. The suite’s scenarios assume a server implementing its own fixture surface – tools named slow_compute, prompts named test_simple_prompt, resources under test://. mcpx is a proxy: it publishes its own small tool set and namespaces every upstream resource as mcpx://<namespace>/<uri>. A scenario that cannot find its fixture fails without ever reaching a protocol assertion. An honest refusal can score worse than a dishonest pass: once #247 made mcpx arrange a real upstream subscription, subscribing to test://watched-resource, which no upstream owns, became a failure (#251).

What the specification asks of a server, revision by revision – including the obligations the prose implies but never states – is docs/spec/server-obligations.md.

2026-09-30T14:30:00-05:00

Decision records

id:           decision-records
since:        pre-release
created:      2026-09-30T10:00:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:process
description:  when two issues answer one question differently, the answer is
              written down before either is built.

An audit of the backlog found eighteen collisions between themes: pairs of issues proposing different answers to the same question, written by people who could not see each other’s work, none with an owner. Each would have been a breaking change whenever the second one was built.

docs/decisions/ holds the answer to each. A record says what the code already does, what each issue proposed, which option was taken and why, and exactly what every affected issue has to change as a result. Three exist: the hook vocabulary and where a hook’s output goes in the exec stream; one autonomy dial with a ceiling the daemon sets; and what mcpx declares against what it enforces.

Two rules keep them honest. A record that lists options without choosing one does not belong here – that is a design document, and it is the shape a decision takes when nobody wants to decide. And a change that contradicts an accepted record changes the record in the same pull request, or it does not merge: a record nobody is obliged to update is a record that describes last month.

2026-09-30T14:30:00-05:00

A command for every operation

id:           a-command-for-every-operation
since:        pre-release
created:      2026-09-30T13:00:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       standard
tags:         area:cli, area:parity
description:  the CLI, /v1, the MCP tools and the plugin are four renderings
              of one declaration, and tests hold each direction.

config = env = cli = /v1 = mcp had been the rule from the start, and the CLI was the surface that kept falling behind: an operation was easy to add to the ops table and easy to forget on the command line, and nothing noticed.

A command is now generated from its operation’s declaration – flags from its parameters, positionals for its path – so it exists the moment the operation does. Sixty operations, each with a route and a plugin method; fifty-nine with an MCP tool, because one of them streams and a tool call cannot. Fifty-two commands, because some cover several operations. docs/parity.md is generated from the same four declarations, which is why it cannot describe a surface that does not exist or leave one out, and ten tests hold the seven directions between them – including that every command the binary runs is declared, and every declared one runs.

mcpx openapi was the same failure one level up. It printed a hand-written map maintained beside the generated one, and the map had drifted to describing a command that was removed: its only declared server was mcpx serve --transport http, which refuses to run, and it listed none of the /v1 operations. Thirteen paths against sixty-four. There is now one document – the command adds to the bytes the daemon serves rather than competing with them – and it stays publishable, because the daemon templates upstream tools as /v1/tools/{tool} instead of enumerating a particular machine’s:

$ mcpx openapi | jq '.paths | length'
64
$ mcpx openapi | jq -c .servers
[{"description":"over the daemon's unix socket","url":"http://mcpx"},{"description":"the loopback endpoint reported by /v1/health","url":"http://127.0.0.1:0"}]

Privileged operations are offered, not hidden. A caller that can reach the socket can already stop the daemon, so withholding the tool buys no safety and costs an agent the ability to restart a server that has wedged. What they carry instead is a description saying so and the MCP annotations (readOnlyHint, destructiveHint) a client can scope on.

2026-09-30T14:30:00-05:00


Proposed

Everything below is wanted and not yet built. Status fields say so.

Plain scripts returning primitives

id:           plain-scripts-returning-primitives
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:scripts

A script’s stdout is already its result, so mcpx run count-todos printing 47 works today. What is missing is a declared contract — an exit-code convention, and --json on run that wraps stdout, stderr, exit status and duration in one envelope for programmatic callers.

2026-09-27T05:05:00-05:00

Named, pinned and resumable sessions

id:           named-pinned-and-resumable-sessions
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:pools, area:sessions

Leases are anonymous and per-run. Wanted: mcpx session new --name login-flow to create a named lease that outlives the process, mcpx session ls and mcpx session search to find them, --session login-flow to rejoin one, and a TTL so a browser mid-login can be resumed rather than rebuilt.

--session <key> already exists on run, exec and call; the missing parts are naming, persistence across daemon restarts, and listing.

2026-09-27T05:05:00-05:00

Attaching to another session

id:           attaching-to-another-session
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:sessions, area:sharing

Read-only attach for watching what another agent’s browser is doing, and read-write attach for taking over. Needs a lease model that admits more than one holder, plus a permission story for who may attach to whose.

2026-09-27T05:05:00-05:00

Per-session restart

id:           per-session-restart
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:pools, area:ops

mcpx restart <ns> stops every instance in a namespace. Wanted: mcpx restart --session <key>, so one wedged browser can be recycled without disturbing the other three.

2026-09-27T05:05:00-05:00

Zero-downtime upgrade

id:           zero-downtime-upgrade
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:daemon, area:ops

Upgrading means stopping the daemon, which kills every MCP server it holds. Wanted: socket handoff to a new binary with child processes preserved — the same problem opencode v2 solves for its PTY daemon with a handoff ticket.

2026-09-27T05:05:00-05:00

Service installation

id:           service-installation
since:        pre-release
created:      2026-09-27T05:05:00-05:00
last-updated: 2026-09-30T14:30:00-05:00
status:       partial
tags:         area:ops, area:packaging

A nix-darwin module existed and was dropped when the package moved into the nix repo. Wanted: mcpx service install emitting a launchd plist or systemd unit.

The systemd half now exists as a template rather than a command. daemon/mcpx.service is a user unit – default.target, Restart=on-failure – with daemon/README.md giving the install, verify and log commands. A user unit rather than a system one because config and state belong in the real $HOME, and a system unit running as root would need a HOME override to find either.

A file you copy is not the same as a command that writes it, so this stays partial: there is still no mcpx service install, nothing emits a launchd plist, and macOS gets nothing. The completions half of the original want is done – mcpx completion bash|zsh|fish exists, and the Nix build already installs what it prints.

Until then the daemon starts on demand from any command, which is the supported path.

2026-09-30T14:30:00-05:00

Alternative input modes

id:           alternative-input-modes
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:cli

Today: mcpx exec '<code>' and mcpx run <name|file>. Scripts inherit stdin, so a script can read it. Missing: mcpx exec - to take the program itself from stdin, --env-file, and an interactive REPL holding one session across statements.

2026-09-27T05:05:00-05:00

OAuth for remote servers

id:           oauth-for-remote-servers
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:transport, area:auth

Streamable HTTP works; bearer headers can be set in config. Missing: the OAuth authorization-code flow, credential storage, and refresh. Servers needing interactive auth must be reached another way for now.

2026-09-27T05:05:00-05:00

Context-budgeted catalog

id:           context-budgeted-catalog
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:discovery, cost:context

mcpx types <ns> is all-or-nothing: 4,402 tokens for chrome_devtools whether three tools are needed or all 27. opencode v2’s code mode instead fits a catalog to a fixed token budget, showing every namespace and as many full signatures as fit, round-robin, shortest first.

Wanted: mcpx catalog --budget 2000 emitting one paste-ready block whose size is bounded no matter how many servers are configured.

2026-09-27T05:05:00-05:00

OpenAPI documents as tools

id:           openapi-documents-as-tools
since:        pre-release
created:      2026-09-27T05:05:00-05:00
status:       proposed
tags:         area:transport

An OpenAPI 3.x document is already a machine-readable description of callable operations. Turning one into a namespace would compose with pools exactly as MCP servers do, and is a large amount of reach for a contained amount of code.

2026-09-27T05:05:00-05:00

Correction, 2026-10-30: this landed. OpenAPI documents are callable tools today — the daemon merges a server’s OpenAPI operations into its tool surface and mcpx openapi prints the generated document — so the proposed status above is the record of the decision, not the state of the code.


Summary

mcpx puts every MCP server behind one command so their schemas stay out of the model’s context until something asks for them, and leases a separate server process to each script run so that concurrent agents driving stateful servers — browsers, above all — cannot corrupt one another.

The walk above is the whole interface: ls to see what exists, search to find a tool, types to load one namespace, run to do the work. Everything else in this document is detail underneath those four verbs.

Notes and links

  • AGENTS.md — the instruction file abridged above. This is the text to inject into an agent’s session; it is deliberately short, and 152 lines today. The quote above is not the whole of it (#264).
  • ASSESSMENT.md — why this exists, with the measurements against the predecessor it replaced.
  • OPENCODE-V2.md — how this compares to opencode v2’s in-process code mode, including where v2 is the better answer.
  • docs/release-runbook.md — how to keep this document current. Read it before editing anything above.
  • docs/proposals.md — open design questions, and what is true today versus what is merely wanted.
  • docs/ideas.md — unvetted brainstorm intake, before anything has a shape, with answers recorded inline.
  • docs/dependencies.md — every library and external program, and why each is needed.
  • docs/remote.md — reaching a daemon on another machine: ssh:// endpoints, ssh -L socket forwarding, ssh -R for a daemon behind NAT, and MCP over ssh.
  • docs/opencode-plugin.md — what the plugin does and why, opencode v1 against v2, and which parts are opencode’s rather than general enough to port to another harness.
  • scripts/stress.sh — concurrency and leak checks against real servers. scripts/bench.sh — latency comparison.
  • Model Context Protocol — the spec. Its client best-practices guide documents this pattern as “programmatic tool calling”.
  • Cloudflare, Code Mode — where the idea was first argued publicly.

mcpx Development Strategies & Lessons Learned

Verification and Diagnostics

  • Never test a system against itself. Driving mcpx’s own server/discover against mcpx proved both halves agreed, not that either was right — they shared the same wrong field name. Test against the schema, the spec, or a reference implementation.
  • The signature defect is declared but not delivered (#177): a flag parsed and dropped, a capability advertised and never sent, a setting read but inert. consumed_test.go proves a setting is read, not that reading it changes anything.
  • The daemon is keyed by its config file set. Three separate bugs came from this; environment differences that should produce different daemons do not.
  • On macOS /tmp is a symlink to /private/tmp and the product canonicalises paths, so a test using os.MkdirTemp("/tmp", …) passes on Linux CI and fails on every Mac.
  • nix build’s checkPhase has had a hand-maintained package list that drifted from the packages that have tests. CI runs go test ./... and does not.
  • The official conformance suite ships its own fixture server. Running it against a different server measures nothing. A falling score can mean the code got more correct — it has, twice.
  • Under parallel agents, load averages above 150 push internal/e2e past its default timeout. Raise -timeout; do not read it as a failure.

Audit: features (end to end)

created:      2026-10-01
base:         origin/main 19237a0
area:         features -- opencode plugin, exec/run, daemon, recipes/prompt, adapters/apis, diagnostics, elicit
method:       each feature run against scratch state (HOME/MCPX_STATE_DIR/MCPX_CACHE_DIR under a temp dir),
              or its code path read end to end; entries already in docs/in-name-only.md not repeated

States: absent, minimal, partial, untested (as in in-name-only.md). Rows marked works are kept as positive controls and are not counted.

Opencode plugin (opencode 1.18.31, plugin types 1.15.5)

Hooks invoked under bun with fake client/ctx against a scratch daemon; not loaded into a live opencode.

whatstateevidenceissue
instructions option (experimental.chat.system.transform)workspushes the guidance text onto output.system (#1938 fix in mcpx-session.ts); v1 type is output: { system: string[] } (@opencode-ai/plugin index.d.ts:264-268). Regression test plugin/opencode/mcpx/instructions.test.ts invokes the hook with v1’s {system: []} shape and asserts the text lands#1938
remember: "permanent" (mcpx_daemon_select, TUI picker)absentputSetting sends persist: true (mcpx/daemon.ts:665-667); daemon field is a string (internal/daemon/routes_settings.go:191). Tool → “Writing the setting failed”; curl same body → cannot unmarshal bool into … .persist of type string
until-gone remembered scopeabsentRemembered.scope written (daemon.ts:1231), never read; after stop --all the file still has "scope":"until-gone" — same as indefinite
toolTiming (“one mcpx stats covers both”)minimalrecord has no duration (mcpx-session.ts:775-781); mcpx log shows harness.tool … tool=bash, mcpx stats calls → “No tool calls in the log yet.”
MCPX_HARNESS_VERSION injectionabsentread from OPENCODE_VERSION (mcpx-session.ts:373,685), which opencode 1.18.31 does not set (env inside it lists OPENCODE_PID etc.)
mcpx_exec tool sessionpartialDaemonClient.exec posts only {source, options} (daemon.ts:484-488), ctx.sessionID never passed (mcpx-session.ts:619); session-scoped pools ignore the tool path (shell path gets MCPX_SESSION_ID)#189
Discovery rung 5 “ask the user”absentdocumented docs/opencode-plugin.md:223; discover skips it (daemon.ts:1119-1122), adapter only toasts (mcpx-session.ts:304-308)
Headless “one warning line into the log”absentdoc opencode-plugin.md:310-313; toast() returns early headless and writes nothing (mcpx-session.ts:294)
annotate “when more than one matched”partialonly when d.ambiguous (rung 6) (mcpx-session.ts:319); with 2 daemons resolved, no mcpx: answered by line
plugin.discoveryRetrypartialenv only (mcpx-session.ts:196), no option despite header “option, env, default” (:65-68); absent from Options and README table
mcpx stats --opencode <dim> (as in skills/mcpx-observability/SKILL.md:50)partialdim stays "" when flag first (cli/logs.go:431-460); stats --opencode --db $DB --top 3 models → overview; --by never passed
mcpx stats opencode <dim> --db/--top/--sincepartialflags after the dim not parsed (logs.go:452-457); --db ignored (“no opencode database found”), --top 2 --since 24h printed all rows, all-time
mcpx_observe errors/slowestpartialreturn "rows": null where others return []
Options bin, binArgs, backend, env, tools, daemonTools; tools discover/exec/observe/daemon_*untestedall ran as documented (works); no test imports mcpx-session.ts; internal/e2e/plugin_test.go:32 runs bun test plugin/opencode/mcpx/ only (31 pass) — the persist mismatch above is what that gap let through#141
headless detection, TUI picker mcpx-tui.tsxuntestedno test; mcpx-tui.tsx not loadable outside opencode (react/jsx-dev-runtime)
internal/opencode (stats over opencode DB)untestedonly opencode.go, no _test.go; nothing in cli/e2e tests exercises it (ran fine against the real DB)

Script execution (exec, run)

whatstateevidenceissue
Permission profiles as a boundarypartiala script reaching the daemon can ask it for more: under --runtime deno --permissions strict, fetch(MCPX_ENDPOINT+"/v1/exec", {source:"await Deno.writeTextFile(…)", options:{permissions:"all"}}) → 200, file written; same under node readnet. /v1/exec does not cap requested permissions
artifact({path}) reads files the profile deniespartialunder strict Deno.readTextFile fails, artifact("leak.txt",{path:"$T/secret.txt"}) succeeds (daemon reads via x-mcpx-path, codegen/emit.go:1355-1358); fetched back [200,"secret-data\n"]. No test for x-mcpx-path
read profilepartial--permissions read + any tool call → cannot reach the mcpx daemon … Requires net access; undocumented (runtimes.md:63-69)
exec.timeout / --exec-timeout (default 120s)partiallocal: --exec-timeout 1s and env both let a 5s script print “late”, rc 0; also not honoured with --remote. Read only at internal/daemon/routes_exec.go:173; CLI --timeout default 0 = none (cli/commands.go:478). No test
Local --timeout messageminimallocal: rc 124, nothing printed; remote prints script timed out after 1s
--suffix “runs last on both paths, like a finally” (settings/registry.go:302)partialexec snippet that throws: suffix never runs (deno/node/bun); run file that throws: runs. Tests cover file path only (e2e/phases_test.go:47)
--remote text outputpartial--remote --format bare 'log.info("hello"); emit(2)' → 2 (local hello 2); renderExecResult ignores res.Logs (cli/exec.go:276-283); stderr dropped ("logs": [])
--remote carries the caller’s environment (exec.md:117-121)partialFOO=bar … --remote --env A=1 'emit([env A, env FOO])' → ["1",null]; only --env sent (exec.go:491)
--remote and caller’s script.profiles/runtimes/runtimeOrderpartialresolved from daemon settings (routes_exec.go:173-178): --permissions mine → no permission profile "mine" remote, works local; runtimeOrder=bun,node → deno remote
diagnose.preflight on --remotepartialremote run proceeds to TypeError: tools.demo.nosuch is not a function; ConsumerPolicy.DiagnosePreflight assigned internal/daemon/consumer.go:121, never read
--hooks-autonomy offpartialrefuses hooks from env/config; hooks given as CLI flags all still run; help does not say
script.typecheckpartialdeno only; node/bun warn and run "str"; deno detected by strings.Contains(base,"deno") (preflight.go:205) so a renamed deno runtime skips it
Generated client result types from outputSchemaabsentstructured(...): Promise<ToolResult>, type ToolResult = any despite an outputSchema; codegen never reads it#216
artifact() misuseminimalartifact({name:"x",data:"y"}) stores a 0-byte _object-Object_ under every runtime instead of throwing
runtimes deno/bun/node, profile enforcement (deno, node), bun refusal, phases, artifacts store/list/get/inline, --keep, --export, mcpx scriptsworksprocess.execPath matched each runtime; read/strict/readnet denials observed; phases fired in order; artifacts listed and fetched

Daemon (pools, cache, reload)

whatstateevidenceissue
pool.max/pool.idleTimeout via settings set (“hot: yes”)partialA pool.* change now reloads: handleSettingsSet → applyPoolRuntime (routes_settings.go:183, :268) → reloadConfig, which calls ApplyPoolSettings (routes_settings.go:659, config/poolsettings.go:36). pool.max reaching live pools is pinned by e2e/poolmax_test.go TestASettingsSetOnTheRunningDaemonReachesEveryLivePool. pool.idleTimeout is in PoolID (config.go:472), so its pool is replaced; reaping after the change is not re-measured. A change whose reload fails is rolled back (routes_settings.go:280), pinned by e2e/poolreload_test.go
min (“keep started even when idle”)partialmin:2 → mn … 0/4 after a reap tick; cfg.Min read only in reap guard (pool.go:992); nothing pre-starts. No test sets it
call scope (and session fallback) frees instances when the call endspartialafter 4 mcpx call ca.state, ca#6/ca#7 still live, idle 55s; only exec/run call ReleaseCaller (commands.go:595, routes_exec.go:202), not CmdCall
pid scope from the CLIpartialkey is the mcpx client’s own os.Getpid() (commands.go:1521) → two calls, two pids: per-call, not per calling program#211
Identically defined servers share one processpartialz and g same command → same PoolID (config.go:357-382); z.open "from-z" then g.state → seen:["from-z"]. Undocumented
Schema history on reload / servers addpartialonly Warm calls ObserveCatalog (registry.go:300, consumer.go:93); added server showed No tools. in history until a second refresh
Config reload failurepartialbad file logged at INFO (“could not be loaded”), hidden by log --level warn; old config kept silently
Per-server pool setting change restarts its processesminimalmax/min/idleTimeout/timeouts/sharing/scope hashed into PoolID (config.go:494-496) → new pool, old drained (reload.go:101, pool.go:1384: calls in flight finish on the old child, up to limits.execDrainTimeout), state lost; observed cw back with 0 tools/instances. Undocumented
shared/exclusive; global/cwd/session/parent-session/repo/worktree; max; idle TTL; restart (all/one/--lazy); stop --all; servers add/remove live; file reload; refreshworksdistinct pids per cwd/session/worktree, shared per repo; exclusive queued 3s; idleTimeout:3s reaped; servers add callable with existing pid unchanged

Diagnostics (diagnose, log, stats, doctor)

whatstateevidenceissue
Failed tool calls (isError) logged as successpartialmcpx call g.boom rc 1, log mcp.call … ok=true, stats calls ERRORS 0; "ok": err == nil (pool/pool.go:959)related #206 (closed)
stats errorspartialfilters level=error (stats.go:308-309); a logged ok:false call → stats calls 100% errors, stats errors “No errors in the log yet.”
stats servers CALLSpartialsums calls from server.stop only (stats.go:185-187): 5 vs 12 in stats calls
Attribution by serverpartialshared-PoolID calls from z logged as server=g (pool.go:956); stats sessions merges global-scope servers into one row
diagnose argument typesabsentg.echo({message: 1}) (string) → “Nothing to report”; no type kind (diagnose.go:28-37); argument-retyped history never checked#111
diagnose unknown namespacepartialnons.x({}) → “Nothing to report”; static check returns nil (diagnose.go:279-283), kind only on runtime path (:501)
diagnose <name> with no such scriptpartialmcpx diagnose zz_no_such_script and help’s mcpx diagnose report → “Nothing to report”, rc 0; falls back to treating name as source (consumer.go:207-210)
doctor server healthpartialnever probes; before refresh only the missing-binary server FAILs, a crashing and an unreachable one pass; after refresh all three FAIL
doctor with invalid configminimalprints parse error, then spawns a daemon on a different socket → three FAILs incl. “daemon did not become ready” from one cause
diagnose missing/unknown/renamed args, unknown tool, history; log record/sql/--chain/--since; doctor -vworkse.g. py2.t: b is required … (schema changed …: `b` became required), rc 1; log sql "delete…" refused

Recipes, mcpx prompt, autonomy

whatstateevidenceissue
Settings flags on prompt as --flag valuepartialmcpx prompt --prompt-autonomy run "…" → want one of propose, run, got "--set"; hoistFlags knows only autonomy/session/set (cli/consumer.go:397)
prompt --mode (decision 0002 table)absentflag provided but not defined: -mode; renamed to --autonomy, noted only in the decision’s “Landed” note
prompt exit status when nothing producedpartialprompt.sample=never → “No recipe matched…” rc 0; sample=ask 5s timeout → “no model answered” rc 0#294
Undeclared recipe valuespartialrecipes run touch-marker tag=c bogus=1 → rc 0, bogus dropped silently
Recipe hole, no TTY/answererpartialblocks 45s, no mcpx elicit answer hint, then fails; not exit 75 like call/exec/run
Recipe hole answered via elicit answerpartialform asked tag; elicit answer <id> tag=zz n=oops accepted, overrode declared n → “n is a number, got "oops"”
recipes CALLS columnminimallists fs.writeFileSync as a call and feeds it into match scoring
CLI tests for ceiling, flag hoisting, exit codesuntestedinternal/recipes/*_test.go covers parse/match only
autonomy.max ceiling on recipes run / prompt --runworksdaemon restarted at each of off…run: only run created the marker; propose/apply printed “clamped to propose”

Adapters and paths.apis

whatstateevidenceissue
Adapter timeoutpartial"timeout":"1s" on sh -c "sleep 5 & wait" → 5.05s, exited -1, never “did not finish within”; ExitError checked before cctx.Err(), no process-group kill/WaitDelay (adapter/adapter.go:255-261). No test
Adapter exit statuspartialprogram exit 3 → “fail exited 3”, rc 1 (cli/adapter.go:155); --json prints no result
Adapter parameter type at call timeabsent{"n":"abc"} for a number → argv abc; buildArgs checks enums only (adapter.go:277-330)
Unknown adapter argumentspartial"bogus":1 ignored, rc 0
Positional starting with -partial{"pos":"--evil"} → argv --evil -v; no -- separator, option injection
Missing adapter programpartialadapter check reports it; mcpx ls lists namespace as unread, 0 tools, no error
API cookie parametersabsentdropped from schema (openapi/openapi.go:467); when passed, sent as query ?sess=C1 (default branch :578)
Required query params / bodyabsentonly required path params checked (openapi.go:565); missing q sent; {} → POST with Content-Length: 0
securitySchemes / securityabsentno reference in internal/openapi or internal/cli; auth only via hand-written headers
Auth with mcpx api --specabsent--spec builds APISpec without Headers (cli/openapi_tools.go:100-106), no header flag; server saw no X-Key
HTTP error exit with --jsonpartialpets_getpet {"id":404} rc 1, with --json rc 0 (openapi_tools.go:181-183)
Spec build errorpartialcontinued (openapi_tools.go:173-175); user sees only “no operation named”
API tool namesminimalgetPet → pets_getpet, pets_getPet not found; description "GET /pets/{id} (GET /pets/{id})"
APIs as namespaces in scriptspartialonly mcpx serve extras (cli/serve.go:529); mcpx ls has no pets, exec 'typeof pets' → undefined (adapters do appear)
CLI/serve API pathsuntestedno _test.go references paths.apis, apiTools, CmdAPI

Elicitation and sampling via the CLI

whatstateevidenceissue
elicit answer key=value typespartialage=3 reaches the server as "3" for an integer field; every value a string (cli/elicit.go:119-126); show’s hint quotes numbers too#255
elicit show constraintspartialshows type/optional/enum, not minLength/minimum#255
Sampling question on the CLIminimalcall el.sample rc 75 prints asks empty and suggests '{}'; elicit show omits the messages (only in --json)#256
ask answers rejectionpartialwrong shape → accepted: 0, problems listed, rc 0, HTTP always 200 (internal/daemon/routes_proto.go:672-676); help does not give the <answers> shape
ask abandon on finished callpartialstatus: completed, rc 0, no indication nothing was abandoned
CLI elicitation testsuntestedno test on JSON types from key=value or on ask answers exit code
exit 75, answer/decline/cancel, re-answer refusedworksserver received accept/decline/cancel; re-answer rc 1

Changes to in-name-only.md entries

  • “Elicitation answering UI … does not show which server asks”: fixed for the CLI — elicit show/list show from el / SERVER el for upstream questions. Schema checking of answers is still absent.

Side findings (outside this area, not traced further)

  • Two daemons on one state dir overwrite daemon.json; stop --all then reported none running while one lived.
  • DELETE /v1/settings/daemon.endpoint?persist=user → "removed":false, key still in config.json.

Skipped

Live opencode instance and TUI picker at runtime; rung-6 ambiguity; stopOthers; opencode v2 sections. Custom --launcher templates, --allow-repeat, --no-capture-console, artifact TTL/quota/GC/put/delete, --exec-output stream remote, --session reuse, non-local daemon.endpoint, tasks via exec. log --follow, log rotation, per-server logging, daemon.leaseTTL, idleExit, protocol: follow lanes, exclusive × non-global scopes. URL-mode elicitation, elicit watch/result, adapter serve, mcpx serve with API tools (read only), Swagger 2 body/formData (read only), YAML specs, --methods filters, prompt.sample=ask with a real model, repair.autonomy. go test was not run.

Most consequential five

  1. Permission profiles do not contain a script that can reach the daemon — /v1/exec accepts permissions:"all" from a strict/readnet script, and artifact({path}) reads denied files.
  2. Failed tool calls are logged ok=true (pool.go:959), so stats calls, stats errors and every error rate built on the log report zero failures.
  3. exec.timeout is not applied — the documented 120s default does nothing locally or with --remote.
  4. Adapter timeout does not stop the program, and adapter/API arguments are not type- or required-checked (positional option injection, cookie params sent as query, missing body sent).
  5. opencode plugin: instructions, remember:"permanent" and until-gone do nothing on v1, and no test loads mcpx-session.ts, which is how they went unnoticed.

Audit: the MCP specification (area spec)

Audited at 19237a0 (origin/main). Authorization excluded (#253). Entries already in docs/in-name-only.md are not repeated.

Commands used (from pkgs/mcpx):

  • go test ./internal/conformance -run 'Test.*/2024-11-05/' -v -count=1 → exit 0, 175 PASS, 5 SKIP, 0 FAIL.
  • go test -count=1 -v -run 'Test(Lifecycle|Messages|Utilities|Features|ClientFeatures|Stateful|Transport)(Server|Client)' ./internal/conformance → exit 0, 28 SKIP (the gapped cells).
  • Same with MCPX_CONFORMANCE_RUN_GAPS=1 → exit 1, 1315 PASS, 27 FAIL. Comparing the two runs: 5 cells skipped as gaps pass.
  • A scratch test (not committed) against the conformance harness’s httpServer: a 2024-11-05-style GET with Accept: text/event-stream returned status=405 body={"jsonrpc":"2.0","error":{"code":-32600,"message":"a GET stream belongs to a session; send initialize first"}}.

How the matrix works matters for every row below: internal/conformance/*_test.go mostly registers one test body for a list of requirement ids (for _, id := range []string{...}; 102 such loops). Each id then reads “tested” in docs/spec/conformance-matrix.md, whether or not the body touches that requirement.

Matrix says “tested”, the cited test does not exercise it

whatstateevidenceissue
Server progress-must-increase, progress-stop-after-completion, progress-only-active-tokens, progress-rate-limit, progress-task-* (MUST/SHOULD, every revision)untestedFixed since: the cells now drive a real mcpclient relay against an upstream that reports badly (serverRelayedProgress), and mcpclient.acceptProgress enforces the rules. At the time of the audit: internal/conformance/utilities_test.go:169-188 calls only mcpx_status and asserts no frame arrives (“mcpx sends none”). But mcpx does send progress: relayed upstream progress on tools/call (internal/mcpclient/relay.go:158-172). That path forwards the upstream’s value verbatim with no check that it increases or is rate-limited, so a decreasing upstream reaches the host as a decreasing mcpx notification. The behaviour that binds mcpx is never looked at.none (#212 closed)
Server logging-setlevel-at-or-above (2024-11-05 – 2025-11-25)untested via cited testCited utilities_test.go:239-262 sets debug and checks that mcpx_status emits nothing; no message is ever filtered. Filtering is real (mcpclient/relay.go:181) and is exercised by an uncited test, internal/mcpclient/relay_test.go:80-86 (LogLevel: "warning"). Matrix points at the wrong test.—
cancellation-max-timeout-despite-progress (both sides, 2026-07-28) and lifecycle-timeouts-max-regardless-of-progress (both sides, 2025-03-26 – 2025-11-25)untestedClient: utilities_test.go:338-360 and lifecycle_test.go:751- use a peer that never answers and sends no progress. Server: utilities_test.go:91-104 checks only that input_required returns fast; lifecycle_test.go:217 uses askUnanswered, no progress. Holds today only because progress never resets any timeout; a change that let progress extend it without a cap passes every cited test.—
cancellation-task-use-tasks-cancel (MUST, both, 2025-11-25)minimalClient cover is the shared timeout body (utilities_test.go:338-345), which asserts the call was not task-augmented. Server cover (utilities_test.go:55-57) uses an un-tasked question. Neither sends a task-augmented request, so the “use tasks/cancel” rule is never reached.#209
lifecycle-http-shutdown-closing-connection for 2024-11-05 (both sides)minimallifecycle_test.go:153-170 initializes, DELETEs with Mcp-Session-Id, expects 404. Its own comment says “(2025-03-26 on)”. 2024-11-05 has no session id, no DELETE and no Streamable HTTP; the requirement is “shutdown = closing the connection”.—
sampling-deprecated-new-impls-should-not-adopt (SHOULD NOT, 2026-07-28)partialCatalogue (docs/spec/requirements.md:727) says “mcpx forwards; pass-through, not adoption”. But the daemon originates sampling: internal/daemon/consumer.go:589-603 (“generation by sampling”), generate → elicit.SampleAsk → broker. And the pool declares sampling to every upstream whenever Hooks.Elicit is set (internal/pool/pool.go:487-494). The cited client test (clientfeatures_test.go:360-375) asserts sampling is declared when a handler exists. So the cited test passes on the behaviour the requirement discourages.#110, #210, #256

Recorded gaps that are already closed (docs contradict code)

whatstateevidenceissue
lifecycle-stdio-client-shutdown-sequence gap (2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25) and stdio-client-shutdown-sequence (2026-07-28)fixed; gap record removed since, the 5 cells runGap text (internal/conformance/coverage_lifecycle.go:98-100, matrix line for every revision): “StdioTransport.Close sends SIGTERM at the same moment it closes stdin”. Code: internal/mcpclient/stdio.go:260-275 closes stdin, waits stdinGrace, then SIGTERM, then SIGKILL after termGrace. With MCPX_CONFORMANCE_RUN_GAPS=1 all 5 skipped cells pass (TestLifecycleClient/{2024-11-05,2025-03-26,2025-06-18,2025-11-25}/lifecycle/lifecycle-stdio-client-shutdown-sequence, TestTransportClient/2026-07-28/transport-stdio/stdio-client-shutdown-sequence). internal/mcpclient/stdio_shutdown_test.go also covers it. Matrix under-reports 5 client cells as gaps; the passing tests are skipped so they guard nothing.#203 (open)

Revisions and transports the suite covers in name only

whatstateevidenceissue
mcpx as a 2024-11-05 server over HTTPpartialNo HTTP+SSE server transport. A 2024-11-05 HTTP client’s opening GET (scratch test above) gets 405 with a JSON-RPC error, no endpoint event. docs/protocol.md:51 and docs/spec/transport.md:9 say “2024-11-05 over stdio and Streamable HTTP”: that is a hybrid no 2024-11-05 client speaks, because Streamable HTTP is 2025-03-26. docs/protocol.md:78 says the same thing (“2024-11-05 has no Streamable HTTP, so nothing arriving on /mcp can be a 2024-11-05 request”). The http-sse-server-* MUSTs are marked n/a (“optional feature”), so the matrix shows 2024-11-05 server MUSTs at 30/32 tested with no transport gap. In practice 2024-11-05 is served over stdio only.#220 is client-side only
2024-11-05 / 2025-03-26 in mcpx’s own suitepartial (not absent)Unlike the official suite, mcpx’s own suite does run per revision: the 2024-11-05 filter above gives 175 passing cells. Most bodies are shared across revisions, though, and the 2024-11-05 HTTP cells exercise 2025-03-26 mechanics (rows above).—

Capabilities declared and not honoured

whatstateevidenceissue
resources.subscribe (legacy)partialDeclared whenever the connection can push (internal/mcpserver/server.go:2153-2157). resources/subscribe always succeeds, including for URIs no upstream owns or whose upstream lacks resources.subscribe. Nothing is delivered for those, and the client is not told (server.go:871-882; the daemon only logs a warning). Deliberate (#251), but the declared capability does not hold for those URIs.#251 (closed)
Relayed progress as mcpx’s own notificationsfixed sinceSee the first table. mcpx is the notifier of record towards the host and passes progress through unvalidated (mcpclient/relay.go:158-172).—

Skipped

  • Did not check each of the ~1300 cells one by one. I read the shared-body loops in utilities_test.go, lifecycle_test.go and clientfeatures_test.go:360-440. Other loops in messages_test.go, features_test.go, stateful_test.go and transport_test.go I only read by header. Many of those are “mcpx emits none, so it cannot emit one wrongly” covers (e.g. features_test.go:180 outputSchema, :358 prompt content, clientfeatures_test.go:296 URL-required errors). They are vacuous but defensible as n/a, so I list none of them individually.
  • Did not diff the schemas in internal/mcpspec/schema/ against what the server emits beyond what the harness already validates.
  • Did not check revision-conflicts.md entry by entry.
  • 27 cells fail under MCPX_CONFORMANCE_RUN_GAPS=1. Those are recorded gaps that are still open, so they are consistent and not listed.

Most consequential five

  1. mcpx cannot serve a real 2024-11-05 HTTP client (405 on the opening GET). The docs call this “2024-11-05 over Streamable HTTP”, a transport that revision does not have, and the matrix shows no gap for it.
  2. The progress MUSTs (increase, stop after completion, active tokens only) are “tested” on a path that sends no progress. The relay path that does send progress forwards upstream values unchecked.
  3. Sampling is adopted, not just forwarded, in 2026-07-28. The daemon originates sampling requests for script generation and declares the capability to every upstream. The matrix marks the SHOULD NOT as tested, using a test that asserts the declaration.
  4. The stdio shutdown gap (#203) is stale (since fixed: the record is removed and the cells run). The fix is in, but 5 conformance cells are still skipped as gaps, so the passing tests guard nothing and the matrix under-reports.
  5. Timeout-despite-progress and tasks/cancel requirements are covered by shared test bodies that never send progress or a task. They would not notice a regression.

Audit: mcpx’s own surface

created:      2026-10-01
base:         origin/main 19237a0
binary:       go build -o /tmp/mcpx-surface ./cmd/mcpx
fixtures:     internal/testsupport/fakemcp, internal/testsupport/askmcp
scratch:      HOME, MCPX_STATE_DIR, MCPX_CACHE_DIR under a private dir
status:       audit only -- nothing fixed

Scope: commands (mcpx --help, mcpx help <cmd>), settings (internal/settings), MCPX_* variables (docs/environment.md), flags, exit codes, and /v1 routes against the CLI. Entries already in docs/in-name-only.md are not repeated; one is re-confirmed below.

How the settings were sampled

The registry holds 173 settings, not ~150: rg -o 'Path: +"[^"]+"' internal/settings -g '!*_test.go' | sort -u = 173. mcpx settings --json shows 129; the other 44 are plumbing-flagged and appear only in <cmd> --help and config --schema.

  1. Static, all 173: for every path, which non-test files name it (rg -F '"<path>"'). Every non-plugin path has at least one accessor read except paths.config (only a ReadBy string in settings/scriptenv.go:177; config discovery is done elsewhere – not traced, so not classified). The twelve plugin.* settings are read by the TypeScript plugin (plugin/opencode/mcpx-session.ts) and were skipped – the plugin cannot be run from here.
  2. Behavioural, 31 settings: run the binary with the setting at two values and compare. Chosen to cover every scope (client, call, daemon), every layer (flag, env, file, mcpx daemon flag, X-Mcpx-Settings header), and every read site that is not the command the help lists it under (where the static scan showed the read happens in a different process from the one the help implies). Verified working: output.json, search.limit, catalog.budget, catalog.bias, daemon.autostart, daemon.endpoint, http.requestTimeout, http.callBodyLimit, script.runtime, script.typecheck, script.permissions (net denies reads; bun/node refuse unenforceable profiles), logging.level and logging.format on exec, logging.file, daemon.watchConfig, events.history, mcp.pageSize, registry.url, plumbing.strictUnknownKeys, pool.callTimeout (env and mcpx daemon flag), artifacts.enabled (env and file), logstore.queryLimit and stats.top (over HTTP), tasks.ttl (exec tasks). Found broken or partial: below.
  3. Not verified: catalog.instructions (fakemcp sends no instructions, so both values print the same), autostart.idleExit (the daemon also waits for no live instances, internal/daemon/server.go:376; not isolated), every proto.*, protoMessages.*, upstream.*, transport.*, prompt.*, recipes.* setting.

Settings

whatstateevidenceissue
exec.timeout (default “120s”, “kill a script after this long”) on mcpx execabsentLocal exec never reads it: cli/commands.go:478 has its own --timeout (default 0 = no limit) and only that reaches the runner. mcpx exec --exec-timeout 1s 'await fake.slow({ms:3000}); console.log("done")' printed done after 3.1s, rc 0; MCPX_EXEC_TIMEOUT=1s same; --exec-where remote --exec-timeout 1s same. The advertised 120s default does not apply locally either. --timeout 1s does kill (rc 124).#77 (call-scoped timeout, related)
Call-scoped settings on POST /v1/exec (exec.timeout, artifacts.delivery, artifacts.inlineMaxBytes, artifacts.interceptImages – all scope: call, hot: true)absentExecService reads s.execSettings() (internal/daemon/routes_exec.go:170-185), a set built once (sync.Once, :53-77) from the config file and environment only – never from the X-Mcpx-Settings header that docs/configuration.md “Scope” says every call-scoped setting is honoured from, and never re-read, so “hot” is false. curl --unix-socket … -H 'X-Mcpx-Settings: {"exec.timeout":"1s"}' -d '{"source":"await new Promise(r=>setTimeout(r,3000))…"}' /v1/exec → exitCode 0, durationMs 3046; the same with "options":{"timeout":"1s"} → exitCode 124, timedOut.–
Exec-service settings given as mcpx daemon --<flag>partialSame cause: execSettings applies file and env, not the daemon’s flags. mcpx daemon --artifacts-enabled=false then artifact("a.txt","hi") in exec → stored (mcpx://artifacts/art-2608…); MCPX_ARTIFACTS_ENABLED=false or {"artifacts":{"enabled":false}} → artifacts are disabled. Also affects script.runtime, script.permissions, script.runtimes, script.captureConsole, script.typecheck, artifacts.* on the remote path (read at routes_exec.go:173-184).–
Daemon-scoped settings given on a client command (call --pool-call-timeout 1s, any of the ~40 daemon settings help call lists)absentAccepted, validated, no warning, no effect – including when that very command auto-starts the daemon (autostart passes daemon --detached plus --idle-exit/--config only, cli/client.go:339-343). mcpx stop --all; mcpx call --pool-call-timeout 1s fake.slow '{"ms":3000}' → slept 3000ms, rc 0. mcpx daemon --pool-call-timeout 1s then the same call → pool.callTimeout exceeded (1s), rc 1. docs/configuration.md “Scope” describes exactly this (“accepted, validated, printed back, and then ignored … Nothing anywhere said so”) as the failure scopes exist to prevent; nothing in internal/cli warns (rg -i 'daemon.scoped|ignored.*running daemon' internal/cli → none).#185 (related)
logstore.queryLimit on mcpx logpartialmcpx log reads the store in-process with its own --limit default 100 (cli/logs.go:83); the setting is only read by GET /v1/log (internal/daemon/ops.go:130). mcpx log --logstore-query-limit 2 → 100 lines; MCPX_LOGSTORE_QUERY_LIMIT=2 mcpx log → 100. Over HTTP with the header → 3 records for 3. help log lists the flag.–
stats.top on mcpx statspartialSame shape: --top default 20 (cli/logs.go:442); setting read only by GET /v1/stats (internal/daemon/ops.go:200-202). stats --by slowest --stats-top 1 and --stats-top 5 → 23 lines each.–
tasks.ttl (“how long a finished task’s result is kept”)partialApplied only to exec tasks (routes_exec.go:233-237). mcpx daemon --tasks-ttl 2s: POST /v1/exec {"options":{"task":{}}} → "ttl":2000; POST /v1/call {…,"task":{}} → "ttl":600000, and the task was still readable after 5s.#87 (related)
script.permissions noneminimalThe profile named none is --allow-all (defaults/defaults.json:43), as are off and unsandboxed. mcpx exec --script-permissions none 'Deno.readTextFileSync("/etc/hosts")' → reads it. A reader picking “none” for no permissions gets all of them.–
mcpx.namespace validation (already in in-name-only)still open{"namespace":"1 bad-ns"} → mcpx ls lists namespace 1 bad-ns, rc 0.–

Commands, help and flags

whatstateevidenceissue
mcpx help <command> “usage, flags and settings for one command”absent (flags)No command’s help has a flags section; it lists settings only. help exec does not mention --timeout, --keep, --session, --export, --ns, --runtime, --format, --prefix… (they appear only in examples prose). Counted for exec, run, call, search, catalog, types, log, stats, restart, stop, doctor, serve, daemon: 0 flag lines each. exec --help (Go’s default usage) does list them, mixed into ~170 settings. man does not mention --keep.#144, #147
Settings listed on every command whether or not it reads themminimalhelp init, help man, help completion, help health, help stop each list the same 71 settings (artifacts-quota, plugin-*, prompt-* …). mcpx man --artifacts-quota 5 is accepted. The list says nothing about which command acts on which.–
Global flags after the commandpartialMain help lists --config, --profile, --skip-default, --all-profiles under GLOBAL FLAGS. --json works after the command; the others do not: mcpx ls --profile nope → flag provided but not defined: -profile, ls --config … and ls --all-profiles likewise.–
--profile <unknown>untested/absent checkmcpx --profile nope ls → full list, rc 0; a misspelt profile is indistinguishable from the default set.–
mcpx resolve <dir> with a broken configpartialresolve on a dir whose .mcpx.json fails to parse prints configPath: empty, socket: …/daemon.sock, rc 0 – a confident wrong answer; mcpx ls in that dir fails. Valid dirs resolve correctly.#88 (related)
mcpx exec --timeoutminimalKills at the limit but prints nothing: rc 124, empty output. /v1/exec with the same limit returns "error":"script timed out after 1s".–
Every other commandworks (smoke)prompt, recipes, scripts, doctor, daemons, adapter, api, servers, schema, client, completion, man, refresh, restart --lazy, status, elicit list, config --schema, health, protocol, resolve, task list/get/result/cancel, artifact list/get/delete, globals, history, tools [--ns], session release, ask poll/begin, tool invoke, complete run and return plausible output or a precise usage error. explore/tui/events (interactive/streaming) skipped.–

Exit codes

whatstateevidenceissue
75 when a call stops for input (help elicit, cli/elicit.go:23-28)partialFirst question on a fresh daemon: mcpx call ask.need_repo → rc 75, mcpx exec 'await ask.need_repo({})' → rc 75. While any earlier question for the same (shared) server is still pending, later callers block instead: exec with one pending → killed by timeout 20 (rc 124); call → killed at 15s; with no outer timeout exec waited 2:00 then printed no repository was chosen (cancel) and exited 0 – the pre-#286 behaviour. mcpx elicit list showed each blocked call’s question was created. Cause not traced; the e2e tests (inputrequired_test.go) start from an empty question store.#286 (closed)
2 for usage errors, 1 for failures, 124 for timeoutworksunknown command → 2; unknown global flag → 2; tool isError → 1; unknown tool → 1; --timeout → 124. Only 75 is promised in docs.–

MCPX_* environment

All 42 names in docs/environment.md resolve to a setter or reader (scriptenv.go registry, plugin, or generated client). Behaviour checked by printing the script’s environment.

whatstateevidenceissue
Variables documented as set “always” for mcpx exec/runpartialLocal exec sets all of them. --exec-where remote omits MCPX_CONFIG_PATH, MCPX_SCRIPT_DIRS, MCPX_EPHEMERAL, MCPX_LOG_LEVEL, MCPX_LOG_SOURCE, MCPX_PARENT_SESSION_ID, and sets MCPX_SESSION_ID to the minted session where the doc says it passes through what it was given (empty). The doc row for each says “always”.#106 (related)
MCPX_CALLn/aDoc itself says it has never existed.–
MCPX_DAEMON_ENDPOINT read by mcpx as daemon.endpointworksMCPX_DAEMON_ENDPOINT=http://127.0.0.1:1 mcpx ls → no daemon answering; it is named explicitly.–
MCPX_STATE_DIR, MCPX_CACHE_DIRworksthe whole audit ran under them; sockets and caches landed there.–
Plugin-set variables (MCPX_HARNESS*, MCPX_OPENCODE_*, MCPX_SESSION_*, MCPX_TRACE_IDS, MCPX_CALL_ID, …)skippedneed the opencode plugin running.–

/v1 routes against the CLI

The OpenAPI document (mcpx openapi) lists 71 method+path pairs; the daemon mux registers 60 (rg 'HandleFunc\("…' internal/daemon). Every registered route is in the document; the 11 extra document entries are POST /mcp and POST /v1/tools/mcpx_*, served by the /v1/tools/{tool} pattern. No route without a handler and no handler without a document entry. Every op has a CLI word (Op.CLIWords, api/ops.go:51), and each generated command smoke-ran.

whatstateevidenceissue
CLI command and its route read different settingspartialmcpx log/mcpx stats read the store locally instead of calling /v1/log//v1/stats, so the route’s setting defaults (logstore.queryLimit, stats.top) and the CLI’s hard-coded defaults (100, 20) differ – see Settings.–
/v1/exec ignores X-Mcpx-Settingsabsentsee Settings; the CLI’s own --exec-timeout cannot reach it either because the CLI never maps it into options.timeout.–
/v1/call tasks ignore tasks.ttlpartialsee Settings.–

Most consequential five

  1. exec.timeout does nothing – not from flag, env, or header; not locally, not remotely. Scripts have no default limit despite the documented 120s.
  2. /v1/exec builds its settings once from file+env: call-scoped “hot” settings in the header are ignored, mcpx daemon --flags are ignored, and runtime changes never land – the scope model in configuration.md does not hold on the exec path.
  3. Daemon-scoped flags on client commands are silently dropped, including when that command starts the daemon – the exact failure configuration.md says the scope field exists to prevent, and help <cmd> advertises ~40 of them.
  4. Exit 75 holds only for the first pending question per server; later callers block until expiry and exec then exits 0 with a cancel result (#286’s original symptom).
  5. mcpx help <cmd> shows no flags, only a 71-plus-entry settings list that is identical for init, man and call; the per-command flags exist only in Go’s raw --help output.

Catalogue

The catalogue chapter: things outside mcpx itself — MCP servers, registries, resources, URLs and projects — and why each one is here.

  • Source this directory, one page per section: servers.md, registries.md, resources.md, urls.md, projects.md.
  • Generated or written: C. The list is hand-written — it says which servers, registries, resources, URLs and projects are in the catalogue and why; the facts per server are captured into a committed snapshot by a refresh script. Nothing at docs build time starts a server or calls the network.
  • First entries are the seven servers configured on this machine — codebase-memory, codedb, fff, fff-nix, fff-worktree, chrome-devtools (the session-mode case) and context7 — plus the official registry registry.modelcontextprotocol.io, the recorded rejection (HAPI), and the projects list from #1867.

The structure is recorded in docs/compendium.md (“The catalogue chapter”); the section pages are seeded by the follow-up task. Approach A (hand-written entries only) and B (mirror mcpx registry search) stay rejected for the reasons on #1867.

Comparisons

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  mcpx next to the five MCP revisions, opencode v1 and v2,
              lootbox and Cloudflare code mode: a granular register of
              every difference, capability matrices, and plain answers to
              the questions the comparison raises.

mcpx sits between things that disagree. There are five revisions of the Model Context Protocol, two of them eras apart. There are two opencodes with different plugin APIs and different ideas of what an MCP server is for. And there are three other ways to run code against MCP tools. This directory records every difference found between them, what each difference is worth to mcpx, and where mcpx stood at one named commit.

It is the “living comparison” that #153 recommends (approach C). The earlier records, ../../ASSESSMENT.md and ../../OPENCODE-V2.md, are left as they were; where this directory finds them wrong it says so (conflicts.md §E).

Start here

documentanswers
history.mdHow mcpx came to exist: the patch given verbatim, lootbox, the rewrite, the opencode discovery, the correction, the timeline, and where it stands.
chapter.mdThe compendium’s comparison chapter: mcpx next to lootbox, opencode code mode, Cloudflare Code Mode, Anthropic programmatic tool calling, mcpac, mcpc and code-mode-mcp as one table, plus where each is better.
stateless.mdCan mcpx speak the stateless 2026-07-28 protocol and every older revision at once? What “stateless” requires, where mcpx keeps state, and what stands in the way.
input-required.mdWhat input_required is, in plain words, with the sequence, and what mcpx does with it in each direction.
content-passthrough.mdDid mcpx need audio, resource_link, structuredContent? What it does with each content type on each path.
conflicts.mdEvery communication conflict: revision against revision, prose against schema, product against product, one mcpx surface against another, mcpx’s documents against its code.
what-a-server-should-be.mdThe character of an MCP server according to each revision and each product.

Matrices

matrixrows
matrix-revisions.mdMCP revisions × every method, notification, capability field, content type, error code and HTTP header
matrix-mcpx.mdmcpx × peer revision, as server and as client, at 05c78b2
matrix-opencode.mdopencode v1 × v2: plugin API and every hook, the MCP client, code mode, the process model
matrix-code-mode.mdlootbox × mcpx × opencode code mode (v1, v2) × Cloudflare code mode

The register

One row per difference: one field, one method, one error code, one header or one behaviour. Each area file has a table (ID, difference, labels, where it exists, mcpx status, value, effort, risk). Below the table is one block per row with the detail: field names, error codes, edge cases, behaviour the specifications imply without stating, and a source for every claim.

areafilerowsmcpx missing
lifecycle and versioningregister/lifecycle-versioning.md4011
transportsregister/transports.md4520
capabilitiesregister/capabilities.md2615
toolsregister/tools.md316
resourcesregister/resources.md1611
promptsregister/prompts.md85
completionregister/completion.md114
pagination and cachingregister/pagination-caching.md147
loggingregister/logging.md93
elicitationregister/elicitation.md238
input_required (MRTR), shared by elicitation, sampling and rootsregister/mrtr.md203
samplingregister/sampling.md112
rootsregister/roots.md51
tasksregister/tasks.md2715
subscriptions and notificationsregister/notifications.md1810
progress and cancellationregister/progress-cancellation.md146
authregister/auth.md253
content typesregister/content-types.md192
errorsregister/errors.md188
_metaregister/meta.md113
plugin APIsregister/plugin-apis.md4112
code mode and script executionregister/code-mode.md5411
process modelregister/process-model.md255
total511171

Two areas are not in the list the register was asked for, because the 2026 revision made them matter: pagination and caching (CacheableResult is new and required) and input_required, which is one mechanism under three features.

Labels

Every row carries every label that applies.

labelmeaningrows
mcpx missingIn scope for mcpx and absent or broken at the named commit. Includes conformance bugs where mcpx claims a feature and gets it wrong171
impl deferredmcpx knows and has deferred it; the row cites the document or comment that says so21
mcpx has, others don'tmcpx does this and the products compared do not18
<rev> hasThe revision in which the thing first appears, and later revisions that change it materially2024-11-05: 74 · 2025-03-26: 39 · 2025-06-18: 42 · 2025-11-25: 62 · 2026-07-28: 152
<rev> removesThe revision that removes it2025-06-18: 3 · 2026-07-28: 48
<rev> deprecatesThe revision that deprecates it (still defined, scheduled for removal)2025-03-26: 1 · 2025-11-25: 1 · 2026-07-28: 22
specs conflictTwo revisions, or the prose and schema of one revision, disagree33
opencode v1 hasopencode 1.18.31 has it100
opencode v2 hasopencode 2.0.3 has it125
lootbox haslootbox has it (the deployed build unless the row says fork)22
has better replacementSuperseded by something better, in a later revision or another product36
has no replacementRemoved or absent with nothing taking its place6
does not match mcpx's goalA feature mcpx should not adopt; the row says why13

Cloudflare code mode has no label of its own. Rows about it name it in where it exists and use the other labels for what it means for mcpx.

The other columns

  • mcpx status (header mcpx @ 05c78b2): ✓ works as defined · partial (the cell says how) · ✗ missing or broken · n/a · acc. accepted beyond what the revision defines (accept liberally), then a few words.
  • Value: + what mcpx gains by doing it, − what it costs or why it is moot; high, med or low.
  • Effort: S (hours), M (days), L (a week or more), XL (a redesign).
  • Risk: what breaks, or who is surprised, if it is done or if it is not.

Where each mcpx missing row is tracked

Every mcpx missing row names its issue in the status cell. Most issues group the rows one fix would close, so there are fewer issues than rows. All are in the milestone mcpx: backlog build-out; the ones where mcpx declares something it does not deliver hang under #177, the rest under #181.

issuetitlerows
#47mcpx: port the opencode plugin to opencode v2 (a rewrite, and several things get better)PLG-01, PLG-02, PLG-13, PLG-27, PLG-30
#77mcpx: protocol follow-ups – inline answers from scripts, cancellation, task status, call-scoped timeoutELI-20, PC-11
#189mcpx plugin: DaemonClient.call sends fields the daemon does not read; mcpx_exec loses the session and reads a missing fieldPLG-39, PLG-40
#199mcpx: serving 2026-07-28 to a conformant client — server/discover fields, serverInfo, cache hints, header checks and HTTP statusesERR-07, ERR-09, ERR-13, LV-22, LV-23, LV-24, LV-26, PG-08, PG-09, PG-10, TR-26, TR-37, TR-38
#200mcpx client: cannot reach a conformant 2026-07-28 serverCAP-03, CAP-09, CAP-11, LOG-02, LOG-07, LV-28, LV-29, PG-02, PG-13, SUB-15, TOOL-09, TR-30, TR-39
#201mcpx: input_required for a stateless 2026 host — a busy loop, a session binding 2026 removed, and flattened resultsELI-10, ELI-17, ELI-18, MRTR-08, MRTR-10, MRTR-19
#202mcpx server: legacy conformance — counter-offer on initialize, 2025-03-26 batches, no replies to notifications, sessions and error codesERR-04, ERR-05, ERR-06, ERR-10, ERR-16, LV-05, LV-06, LV-07, PG-03, SUB-18, TR-05, TR-09, TR-27, TR-28, TR-35, TR-36
#203mcpx client: legacy Streamable HTTP and stdio robustness — session 404, GET stream, SSE retry, string ids, stream timeout, version headerLV-08, LV-39, PC-08, PC-09, PC-13, PG-07, TR-14, TR-22, TR-29, TR-34, TR-44, TR-45
#204mcpx: validate Origin on /mcp and /v1, and refuse cross-origin simple requests to /v1/execAUTH-23, AUTH-24, TR-41
#205mcpx: the per-server auth: block is parsed and never appliedAUTH-22
#206mcpx: mcpx_call reports upstream isError as success, and every proxied result is flattened to textCT-06, CT-13, META-11, PRM-07, RES-09, RES-10, TOOL-26
#207mcpx: resources/templates/list sends uri instead of uriTemplate, upstream list metadata is dropped on parse, and titles leak to 2025-03-26PRM-01, PRM-02, PRM-03, PRM-04, RES-01, RES-02, RES-03, RES-04, RES-05, RES-06, RES-07, TOOL-17, TOOL-20, TOOL-22
#208mcpx: subscriptions and list_changed — declared on stdio and never delivered, 202 over HTTP, and the 2026 wire shapeCAP-15, CAP-17, CAP-18, CAP-20, RES-14, RES-15, SUB-03, SUB-04, SUB-05, SUB-08, SUB-09, SUB-10, SUB-11, SUB-12
#209mcpx: tasks — the 2026 extension is advertised and not implemented; core tasks are unbound and live in two storesCAP-21, CAP-22, CAP-24, CAP-25, CAP-26, TASK-01, TASK-03, TASK-04, TASK-05, TASK-06, TASK-07, TASK-08, TASK-11, TASK-12, TASK-13, TASK-15, TASK-18, TASK-22, TASK-24, TASK-27, TOOL-21
#210mcpx: capabilities declared to upstreams that nothing answers, and host gates that ignore sub-capabilitiesCAP-02, CAP-04, CAP-08, ROOT-04, SMP-09, SMP-11
#211mcpx: take the pool’s session key from the request (_meta["ai.opencode/sessionID"]), not the daemon’s pidMETA-09, PM-10
#212mcpx: relay progress, log messages and trace context between hosts and upstreamsCODE-13, LOG-08, META-07, PC-04, PC-05
#213mcpx: completion/complete over MCP ignores ref and never asks the upstreamCMP-01, CMP-05, CMP-09, CMP-10
#214mcpx: elicitation edges — enum downgrade for 2025-06-18, url mode upstream, -32042, and a script-side answer hookELI-04, ELI-09, ELI-12, ELI-21
#215mcpx codegen: JavaScript reserved words, prelude-name clashes and colliding tool namesCODE-40, CODE-41, CODE-42
#216mcpx exec: typed results from outputSchema, an output cap, the final expression as the result, typed failures, paged searchCODE-16, CODE-19, CODE-30, CODE-35, CODE-36, CODE-44
#217mcpx plugin: the instructions option has never worked on opencode v1PLG-16
#218mcpx and opencode code mode: nested code mode, ?codemode=false, and what a harness expects of mcpx_execCODE-53, PLG-35, PLG-36, PLG-37, PLG-38
#219mcpx: /v1/exec leaves session-scoped servers running, /mcp never rebuilds its surface, /v1/protocol reports the default nativeElicit, stats reads one opencode databasePM-12, PM-13, PM-15, PM-21
#220mcpx client: the deprecated HTTP+SSE transport, for remote servers that still speak itTR-10, TR-11

Refreshing the status column

The status column is a snapshot of mcpx at 05c78b2, and mcpx has changed since – including fixes to rows in every area. Treat every mcpx @ 05c78b2 cell as out of date until re-checked; some are wrong in mcpx’s favour. They are not refreshed piecemeal, because refreshing a 511-row register by hand is how a register starts lying: it is better to know the whole column is stale than to trust a column that was half updated.

What has landed since is not listed here, because a hand-kept list goes stale the same way. Ask git:

git log --oneline 05c78b2..HEAD -- pkgs/mcpx

For the protocol rows, the current status of each specification requirement is ../spec/conformance-matrix.md, which is generated from the tests and checked on every go test.

To refresh:

  1. Pick the new commit. Replace 05c78b2 in the header of each table and in the mcpx @ field of each block you re-check.
  2. For each row, re-read the mcpx path:line citations in its block. The status cell and the mcpx @ field are the only parts that describe mcpx. Everything else describes a specification or another product, and stays true until that changes.
  3. Where the fix landed, set the cell to ✓ and drop mcpx missing from the labels. Keep the row: the difference between the revisions is still real.
  4. Update the counts above.

matrix-mcpx.md has the same property: each row cites the code it describes.

Citations

Every claim has a source, in one of these forms:

formmeans
schema/<rev>/schema.ts:Nthe schema of that revision, in modelcontextprotocol/modelcontextprotocol at 046fa30
<rev>/<path>.mdx:Nspecification prose, docs/specification/<rev>/<path>.mdx in the same repository; public at https://modelcontextprotocol.io/specification/<rev>/<path>
seps/<file>.md:N, docs/extensions/…:N, docs/docs/…:NSEPs, extension pages and guides in the same repository
internal/…:N, docs/…:N, plugin/…:Nmcpx, relative to pkgs/mcpx at 05c78b2
v1:packages/…:Nopencode 1.18.31, /nix/store/xczwr0rfqkd99yjg4aqv28yfxhim4pg1-source
v2:packages/…:Nopencode 2.0.3, nix eval --raw .#opencode2.src (/nix/store/y08jwgxhq18hm706fisnqx68n39bid9i-source)
.lootbox/…:N, nix:…:Nfiles in this repository at 05c78b2 (lootbox’s fork checkout is .lootbox/; the deployed lootbox is upstream 587a5a1 plus the patches in flake.nix)
@modelcontextprotocol/sdk@1.22.0/…:Nthe MCP SDK lootbox resolves, from the Deno npm cache
https://unpkg.com/… (Ln)an npm package opencode pins but neither tree vendors
a URL and a section headingCloudflare documentation or blog posts, fetched 2026-09-30
wire W20, M1, S2, …a recorded session against a binary built from 05c78b2, with isolated state, driving the repository’s test servers; see stateless.md

Quoted text after a citation is verbatim from that line. Four quotes of specification prose have relative links expanded to absolute ones.

  • ../protocol.md: mcpx’s own protocol design (accept liberally, send conservatively, one downgrade() at the edge). This directory records differences and conflicts; it does not restate that design.
  • docs/spec/server-obligations.md: the exhaustive list of what a server MUST and SHOULD do per revision. The protocol work is writing it, and it is not on main at 05c78b2. what-a-server-should-be.md summarises the character of each revision and product, not the obligations.
  • ../opencode-plugin.md: the plugin’s design. Claims in it that the pinned sources contradict are in conflicts.md §E.

The comparison chapter

created:      2026-10-29T19:20:00-05:00
last-updated: 2026-10-29T19:20:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  mcpx next to lootbox, opencode code mode, Cloudflare Code Mode,
              Anthropic programmatic tool calling, mcpac, mcpc and
              code-mode-mcp as one table plus prose, every row stamped with
              what it was checked against, including where the others are
              better.

This is the presentation the compendium asks for: one table, then one section per neighbour, honest about where they are better. The details live in the register and the matrices; this page is the reading copy, and every row says what it was checked against so a stale row is visible as a stale row.

How to read a row. The mcpx cell carries mcpx @ 5a34fa70 — checked against the code at that commit on 2026-10-29. The other cells carry the date of the last check recorded in the register, 2026-09-30, or the date the upstream documentation was fetched. A row whose stamp is older than your copy of the other project may be out of date: re-check it, do not assume it.

The table

mcpx @ 5a34fa70lootbox (deployed 587a5a1+4 patches)opencode code mode (v1 1.18.31 / v2 2.0.3)Cloudflare Code ModeAnthropic programmatic tool callingsh1nduu/mcpacapify/mcpctmustier/code-mode-mcp
ShapeCLI + daemon, generated TypeScript client, MCP server modeDeno daemon + CLI, launchd singleton on :9420a feature inside opencode’s agent loop (v1 behind OPENCODE_EXPERIMENTAL_CODE_MODE, v2 on by default)a feature of the Agents SDK on Workersa hosted API feature of Claude’s tool useCLI: generate TS types, executeCLI MCP clientstdio MCP server exposing one exec tool
Who invokesshell: any agent, script, human, CIshell / HTTPthe model, via executethe model, via one toolClaude, via code_executionshellshellthe model
Runtimereal deno / bun / node subprocessDeno subprocesspurpose-built JS interpreter in-process (v1: no classes or generators; v2: generators, no classes)V8 isolate via the Worker Loader APIPython in Anthropic’s containerBunnone (shell + --json)Node
Confinementnot “none”: script.permissions defaults to all and has profiles (net, read, read-net, strict), taking effect under Deno — an audited, opt-in boundaryDeno permission flags; filesystem and subprocess off, network open by defaultstrong: only implemented language features exist; no filesystem, no networkreal isolation: isolate, fetch()/connect() throw, bindings hide credentialsmanaged sandbox, zero infrastructuredeclared + granted capability lists, checked against the script’s declarationn/anone: “same authority as the Node.js process”
Discoveryls / search / types outside the context, plus in-script search() / describe()recomputed on every request — the bug that started all thisbudgeted catalog + in-program search(), diff updateswhole generated API in context; the post calls search a “future improvement”tool search is a separate platform featureCLI explorationgrep, tools-list, tools-getsearch() / describe() in the program
Stateful server isolationshared / pooled / session pools, one process per runnone: one shared client, one Chromeone connection per server per workspace; _meta sessionID hintper agentn/anot describedpersistent named sessionsin-memory JSON session state
Outside a model turnyesyesnononoyesyesonly via an MCP client
Where it is better—nothing we still needconfinement by construction; the catalog budgeting mcpx copiedreal isolation; credentials never reach the codezero infrastructure; allowed_callerscapability grants checked against a declarationthe widest protocol coverage on a CLI, including OAuth 2.1OAuth, sampling and elicitation as a drop-in MCP server

Stamps: mcpx column and the confinement correction at 5a34fa70 (2026-10-29), from register/code-mode.md and matrix-code-mode.md; the opencode, lootbox and Cloudflare columns as recorded in the same matrix (checked 2026-09-30 against opencode 1.18.31 and 2.0.3, lootbox 587a5a1+4 patches, and Cloudflare’s documentation); the three search-found projects from their READMEs as fetched that day; Anthropic from https://www.anthropic.com/engineering/code-execution-with-mcp (2025-11-04) and https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling.

The earlier seed table in #1869 said mcpx’s confinement is “none, by choice”. That was wrong when it was written: script.permissions exists with four profiles and is enforced under Deno (internal/settings/registry.go:183, ../container.md §2). The default is still all, which is a stance, not an accident — see the divergence below.

The pattern, and where mcpx diverges from it

The MCP client best-practices guide (https://modelcontextprotocol.io/docs/2026-07-28/develop/clients/client-best-practices) is not a product here: it is the pattern’s specification — progressive discovery (catalog, inspect, execute) plus programmatic tool calling — and it says the pattern “requires clients to implement a sandbox environment” with no direct network access. mcpx deliberately has none; ASSESSMENT.md records the instruction as “I dropped the sandbox, as you asked”. That divergence is stated here rather than buried: mcpx is a tool for an owner who wants a script to have the machine’s authority, and every neighbour above with real confinement is making a different trade.

The neighbours, one section each

lootbox

The predecessor. Same shape (daemon + CLI + generated types), same purpose, and the reason mcpx exists. Its discovery recomputed every request, it was killed by the OOM reaper under load, its artifact was 158 MB, and one Chrome served every agent — the last is architectural, not a bug to fix. Nothing it did is still missed. Checked 2026-09-30; it has had no upstream commit since 2026-02-27.

opencode code mode

The one that nearly made mcpx pointless, and the one mcpx learned from. Where it is better: confinement comes free, because a purpose-built interpreter simply does not implement fetch, Deno.run or a filesystem; and the catalog budget with in-program search() was the model mcpx copied. Where it is not: it only exists inside a model turn, and it keeps one MCP connection per server per workspace, so concurrent Chrome work is still shared. The 2026-09-29 correction means v1 has code mode too — the earlier claim that v1 users had nothing is wrong. Checked against opencode 1.18.31 and 2.0.3 (2026-09-30).

Cloudflare Code Mode

Where it is better, plainly: real isolation with a per-pass V8 isolate, network blocked or routed through a host Fetcher, and credentials that never reach the code. It also has CPU, subrequest, output and durable-value limits that mcpx does not have any of. Where it is not: it is a hosted product feature, not something a human or a CI job can call from a shell. Checked against Cloudflare’s Code Mode documentation (2026-09-30); the original argument is the 2025-09-26 post, https://blog.cloudflare.com/code-mode/.

Anthropic programmatic tool calling

The pattern with zero infrastructure: the model writes Python in a managed sandbox and calls tools as functions. Better at exactly that. Not comparable as a tool — there is no CLI, no daemon and nothing to run outside a model turn, and its own documentation says tools “provided by an MCP connector” cannot be called programmatically, which is the gap mcpx fills. Checked 2026-09-30.

sh1nduu/mcpac

A Bun CLI that generates TypeScript types and executes. Better at capability grants: the script declares what it needs and the grant list is checked against that declaration, which is a more interesting model than mcpx’s all-or-nothing default. Checked against its README 2026-09-30.

apify/mcpc

A CLI MCP client with the widest protocol coverage of the group on a command line, including OAuth 2.1, plus persistent named sessions. Better at protocol coverage. It does not run code, so it is a complement rather than a rival: it is also where to look for the longest maintained table of MCP CLI clients, which is why this page links it instead of copying it. Checked against its README 2026-09-30.

tmustier/code-mode-mcp

The inverse shape: a stdio MCP server that exposes one exec tool, so the model gets code mode through the protocol rather than through a harness. Better at being a drop-in — OAuth, sampling and elicitation come with being a server. Worse at isolation: it says the script has “same authority as the Node.js process”. Checked against its README 2026-09-30.

Re-checking

Rows are re-checked when the compared project moves or when mcpx does, and a re-check is stamped with the commit or date it used. The mcpx column has the same problem the register’s mcpx @ column has: it is a snapshot, and the code moves faster than this page. When a stamp here is older than the register’s, the register is right.

Communication conflicts

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  every place two parties cannot both be satisfied: revision
              against revision, prose against schema, product against
              product, one mcpx surface against another, and mcpx's
              documents against its code.

A conflict here is a place where doing what one side expects breaks another. Some are in the specification itself. Some are between the products mcpx sits between. Some are inside mcpx. mcpx’s design answer to the protocol-level ones (accept liberally, send conservatively, one downgrade() at the edge, two mechanisms for one question) is in ../protocol.md and is not repeated here. This page records the collisions, not the design. mcpx status is as of 05c78b2.

Each entry names both sides, the source for each, and what mcpx does or should do. IDs are stable so issues and register rows can point at them.


A. Revision against revision

A dual-era server has to serve both sides of each of these, per request.

C-01 A request with neither _meta nor a session. 2026-07-28 calls it malformed and requires -32602 with HTTP 400 (2026-07-28/basic/index.mdx:380-382). The legacy HTTP rules say a server that receives no MCP-Protocol-Version “SHOULD assume protocol version 2025-03-26” (2025-06-18/basic/transports.mdx:250-254). The specification never says what a dual-era server should do. mcpx serves it as 2025-03-26 (internal/mcpserver/conn.go:137-146). That is right for a dual-era server, and it should stay. See stateless.md §4.

C-02 Version negotiation. Every legacy revision says the server MUST answer initialize with a version it supports, a counter-offer (2025-03-26/basic/lifecycle.mdx:130-133). 2026-07-28 answers an unsupported version with an error, -32022 (2026-07-28/basic/versioning.mdx:48-67). mcpx applies the 2026 behaviour to legacy initialize: 2024-11-05 and unknown versions get -32022, a code no legacy client knows (internal/mcpserver/server.go:844-859, W1). It should counter-offer on initialize and keep -32022 for _meta requests.

C-03 Resource not found. -32002 through 2025-11-25; -32602 in 2026-07-28, which also forbids emitting -32002 and asks clients to still accept it (2026-07-28/basic/index.mdx:136-142). mcpx sends -32602 to every revision, so legacy clients get the wrong code (internal/mcpserver/server.go:713-716).

C-04 Logging’s default flipped. 2025-11-25: with no logging/setLevel “the server MAY decide which messages to send automatically” (schema/2025-11-25/schema.ts:1545). 2026-07-28: with no per-request logLevel the server “MUST NOT emit notifications/message” (2026-07-28/server/utilities/logging.mdx:62-64). mcpx emits no log messages to hosts at all, so it satisfies both by silence. Its own comment states the 2026 rule backwards (internal/mcpserver/revisions.go:72-73).

C-05 How a server asks. Legacy: a server-initiated request on the open connection. 2026-07-28: input_required and a retried request (2026-07-28/basic/patterns/mrtr.mdx:7-14). Two mechanisms, one question. mcpx bridges them both ways (input-required.md).

C-06 Resource subscriptions. resources/subscribe and unsubscribe through 2025-11-25; subscriptions/listen with a resourceSubscriptions filter in 2026-07-28 (2026-07-28/changelog.mdx:18). The capability flag resources.subscribe survives in the 2026 schema with a new meaning, “supports resourceSubscriptions on listen” (schema/2026-07-28/schema.ts:850, 2026-07-28/server/resources.mdx:58). mcpx accepts both from either era.

C-07 Tasks, core against extension. The 2025-11-25 experimental tasks and the 2026 io.modelcontextprotocol/tasks extension are “not wire-compatible” (seps/2663-tasks-extension.md:944). tasks/result and tasks/list are removed and tasks/update added. failed changes meaning: in core it includes tool results with isError; in the extension those are completed (schema/2025-11-25/schema.ts:1310, seps/2663-tasks-extension.md:835). mcpx serves the core shape to every era and advertises the extension (internal/mcpserver/server.go:1404-1417).

C-08 _meta key prefixes. 2025-06-18 recommends forward DNS (example.com/); 2025-11-25 recommends reverse DNS (com.example/) (2025-11-25/basic/index.mdx:207-210). Neither changelog mentions it. mcpx reserves nothing of its own in _meta. opencode’s ai.opencode/sessionID follows the reverse rule.

C-09 Batching lived for one revision. 2025-03-26 servers “MUST support receiving JSON-RPC batches” (2025-03-26/basic/index.mdx:97); 2025-06-18 removes batching (2025-06-18/changelog.mdx:12). mcpx lists 2025-03-26 as supported and rejects batches with -32700 (W16).

C-10 Is a dropped connection a cancellation? 2025-03-26 through 2025-11-25: “Disconnection SHOULD NOT be interpreted as the client cancelling its request” (2025-11-25/basic/transports.mdx:128). 2026-07-28: closing the response stream “MUST be treated by the server as cancellation” (2026-07-28/basic/transports/streamable-http.mdx:235-236). mcpx propagates the HTTP request context to the upstream call for every era. It is right for 2026 and against the legacy SHOULD NOT. This is inferred from the context chain and was not driven.

C-11 notifications/cancelled.requestId. Optional in 2025-11-25 (so a task can be cancelled without one), required again in 2026-07-28 (schema/2025-11-25/schema.ts:223). mcpx reads it when present.

C-12 URL elicitation’s plumbing. 2025-11-25 added elicitationId, notifications/elicitation/complete and -32042; 2026-07-28 removed all three (2026-07-28/changelog.mdx:54-60, 2026-07-28/basic/index.mdx:144). mcpx passes an upstream’s elicitation params through to a 2026 host verbatim, so a 2026 host can receive an elicitationId it has no use for.

C-13 Sessions. Legacy: a client “MUST include” the Mcp-Session-Id it was given (2025-11-25/basic/transports.mdx:206-208). 2026-07-28: a server supporting only that revision should “ignore it, and do not mint or echo session IDs” (2026-07-28/basic/transports/streamable-http.mdx:680-686). mcpx mints one on server/discover, a modern request, and its modern input_required path needs it back (stateless.md).

C-14 MCP-Protocol-Version. Legacy (2025-06-18+): required after initialization; an invalid value is a 400 (2025-06-18/basic/transports.mdx:256-257). 2026-07-28: required on every POST and must equal the _meta version, else -32020 (2026-07-28/basic/transports/streamable-http.mdx:252). The 2026 page also says revisions before 2025-06-18 “did not define” the header (2026-07-28/basic/transports/streamable-http.mdx:278). 2025-03-26 did define it, for OAuth metadata discovery (2025-03-26/basic/authorization.mdx:135-139). mcpx ignores an invalid legacy header (W14) and answers a 2026 mismatch with a non-JSON-RPC 400 (W10).

C-15 structuredContent and outputSchema. An object, described by an object-rooted schema, in 2025-06-18 and 2025-11-25; any JSON value, any schema in 2026-07-28 (schema/2026-07-28/schema.ts:1821). A server that returns an array to a 2026 client must not return it to a 2025 one. mcpx emits no structuredContent today.

C-16 Client registration. Dynamic Client Registration is a SHOULD in 2025-03-26 (2025-03-26/basic/authorization.mdx:42), a MAY in 2025-11-25 (behind Client ID Metadata Documents), and deprecated in 2026-07-28 (2026-07-28/deprecated.mdx:29). mcpx performs no OAuth.

C-17 includeContext. thisServer and allServers are soft-deprecated in 2025-11-25 and deprecated in 2026-07-28 (2026-07-28/changelog.mdx:84-88), gated by sampling.context in between. mcpx forwards sampling requests to hosts without checking it.

B. One revision against itself

C-20 Bad tool arguments: protocol error or tool error? 2026 prose makes input-validation failures isError results (2026-07-28/server/tools.mdx:760-765). So does the 2025-11-25 changelog (2025-11-25/changelog.mdx:30). The 2026 schema’s InvalidParamsError still lists “invalid tool arguments” as -32602 (schema/2026-07-28/schema.ts:366) and ships a -32602 example for it. mcpx reports its own tools’ argument errors as isError results (internal/mcpserver/server.go:665-675), which follows the prose.

C-21 Ending a subscription (2026). The cancellation page says the server MUST send notifications/cancelled for the listen id (2026-07-28/basic/patterns/cancellation.mdx:11). The subscriptions page says it SHOULD send a successful subscriptions/listen result, then close (2026-07-28/basic/patterns/subscriptions.mdx:122-124). The schema limits the server-sent cancel to stdio (schema/2026-07-28/schema.ts:637).

C-22 Client cancellation (2026). “A client SHOULD send a cancellation notification” (2026-07-28/basic/patterns/cancellation.mdx:8), but on HTTP the transport “defines no client-to-server” notification. Closing the stream is the cancellation (2026-07-28/basic/transports/streamable-http.mdx:95-101). Read the SHOULD as stdio-only.

C-23 Legacy lifecycle against its own example. A counter-offer is a MUST (2024-11-05/basic/lifecycle.mdx:126), yet the same page’s error example answers an unsupported version with -32602 (2024-11-05/basic/lifecycle.mdx:210). The client’s disconnect is a SHOULD in prose (:130) and a MUST in the schema (schema/2024-11-05/schema.ts:162).

C-24 Which results are cacheable (2026). The changelog names five methods (2026-07-28/changelog.mdx:36); the caching page and the schema add server/discover (2026-07-28/server/utilities/caching.mdx:16, schema/2026-07-28/schema.ts:678).

C-25 Where inputResponses may go (2026). The schema says “any client-initiated request” (schema/2026-07-28/schema.ts:598); the prose allows input_required on three methods only (2026-07-28/basic/patterns/mrtr.mdx:192).

C-26 Is resultType open (2026)? Typed as "complete" | "input_required" | string (schema/2026-07-28/schema.ts:216), but “a resultType of any value unrecognized by the client MUST be considered invalid” (2026-07-28/basic/index.mdx:84). An extension value ("task") is valid only where the extension was negotiated. mcpx stamps "complete" on its task handle, where the extension’s value would be "task".

C-27 The examples break their own rule (2026). The list_changed examples carry no _meta (2026-07-28/server/tools.mdx:246), though every notification on a listen stream MUST carry io.modelcontextprotocol/subscriptionId (2026-07-28/basic/index.mdx:413). An implementer copying the example (as mcpx’s stdio listen path effectively does) is non-conformant.

C-28 The tasks SEP against itself. tasks/get responses MUST use resultType: "complete" (seps/2663-tasks-extension.md:340), but its own error examples show "task" (:844, :868). It names the protocol version 2026-06-30 (:17). It reserves notifications/tasks/ but names its notification notifications/tasks (:894, :422).

C-29 “Unsolicited” tasks (2026). The changelog says servers may return task handles “without per-request opt-in” (2026-07-28/changelog.mdx:22). The extension still requires the client to list it in each request’s capabilities (docs/extensions/tasks/overview.mdx:231-233). Both hold once “opt-in” is read as “the removed task parameter”.

C-30 Answers cannot be errors (2026). InputResponse has no error variant (schema/2026-07-28/schema.ts:541-542). Yet the schema still documents client-side -32602 for elicitation modes and sampling tool results (schema/2026-07-28/schema.ts:370-371), errors a client can no longer return inside MRTR.

C-31 Deprecation windows (2026). Twelve months minimum (2026-07-28/changelog.mdx:110), except HTTP+SSE: three months after SEP-2596 reaches Final (2026-07-28/deprecated.mdx:31).

C. Product against product

C-40 Neither opencode speaks 2026 by default. v1’s SDK negotiates 2025-11-25 and has no modern path (v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:36). v2 speaks 2026-07-28 only when a server is configured protocol: "auto" or "2026-07-28" (v2:packages/schema/src/mcp.ts:20-23). So mcpx’s modern serving path is not exercised by either host out of the box. docs/opencode-plugin.md says v2 will exercise it; that holds only with the option set.

C-41 Session identity arrives two ways, and mcpx reads neither on /mcp. The v1 plugin injects MCPX_SESSION_ID into shell commands through shell.env (plugin/opencode/mcpx-session.ts:664). That reaches the CLI, not /mcp. v2 has no session id in its shell hook, and instead sends _meta["ai.opencode/sessionID"] on every tools/call (v2:packages/core/src/mcp/client.ts:278). mcpx’s MCP server keys every call by process id (internal/cli/serve.go:292-297).

C-42 Code mode inside code mode. v2 wraps every MCP server in its own code mode unless the server is configured codemode: false (v2:packages/schema/src/mcp.ts:34-35), and the built-in exclusion matches only executor (v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:16). mcpx’s mcpx_exec then runs as a tool called from a program the model wrote for opencode’s interpreter. v2 also appends ?codemode=false to remote MCP URLs so servers that bundle code mode can expose raw tools (v2:packages/core/src/mcp/client.ts:214-218). mcpx ignores the parameter.

C-43 Tool names. opencode names MCP tools <server>_<tool> (v1:packages/opencode/src/mcp/catalog.ts:119), so mcpx configured as mcpx becomes mcpx_mcpx_namespaces. v2 rejects any name part over 64 characters (v2:packages/core/src/tool.ts:297).

C-44 v1 drops what mcpx’s artifacts use. v1’s direct tool path keeps text, images and embedded resources and drops audio and resource_link (v1:packages/opencode/src/session/tools.ts:429-436). mcpx_exec artifacts are resource_link blocks, so under v1 without code mode the model never sees them.

C-45 Declaring resources costs every v1 request context. v1 adds three resource tools to every request when any server declares resources (v1:packages/opencode/src/session/tools.ts:136). mcpx always declares it.

C-46 Nobody answers mcpx’s questions on v1. v1 has no elicitation/create handler and declares no elicitation (v1:packages/opencode/src/mcp/index.ts:42-48, :77). mcpx correctly never sends it one, so a question raised under v1 goes to the broker. v2 declares form and url and answers them, but attributes them to the Location, not the session (v2:packages/core/src/mcp/index.ts:80-82).

C-47 Progress that keeps nothing alive. v2’s comment says requesting progress “keeps long calls alive under the SDK’s timeout”, but resetTimeoutOnProgress is never set (v2:packages/core/src/mcp/client.ts:279-281). mcpx never forwards progress, so in either host a long mcpx_call is bounded by the host’s timeout: 60 s on v1 unless configured, 12 h on v2.

C-48 lootbox declares nothing. lootbox’s MCP client (SDK 1.22.0, latest 2025-06-18) sends capabilities: {} (.lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153). An upstream that elicits gets method-not-found. mcpx declares elicitation and answers through its broker.

C-49 Sandbox or no sandbox. The MCP client best-practices guide’s programmatic tool calling “requires clients to implement a sandbox”. Cloudflare argues a shell-based CLI is “a much broader attack surface than a sandboxed isolate”. opencode runs only the language features it implements. mcpx runs scripts on a real runtime with full authority, by choice (register/code-mode.md). Nobody is wrong on the facts. The disagreement is about who the caller is and what it may do.

D. One mcpx surface against another

C-60 mcpx_call against the script client. The same upstream result is seen differently. mcpx_call drops isError, pretty-prints structuredContent as text and drops images (internal/cli/commands.go:384-415). A script gets ToolError, the structured value, and every block on .raw (internal/codegen/emit.go:493-518). See content-passthrough.md.

C-61 Two task stores. The comment on the task store says one store serves both the MCP server and /v1 so a task started on one is visible on the other (internal/tasks/tasks.go:9-12). There are two: the MCP server’s (internal/mcpserver/tasks.go:31-38) and the daemon’s (internal/daemon/ops.go:428-429).

C-62 stdio and HTTP declare different capabilities. The same server declares listChanged: true and subscribe: true over stdio and false over HTTP (W2, S2). Neither delivers: nothing starts a notifier for a legacy stdio client (internal/mcpserver/server.go:1389-1402).

C-63 mcpx’s server and client agree with each other and not with the schema. Both use protocolVersions for server/discover (internal/mcpserver/server.go:534, internal/mcpclient/client.go:284), and three tests pin it (internal/mcpserver/server_test.go:393, internal/mcpclient/era_test.go:87, internal/mcpclient/modern_test.go:88). Self-consistent tests are how an interoperability bug passes CI.

C-64 The plugin’s mcpx_exec and the daemon’s /v1/exec. The plugin reads an output field the daemon’s result does not have, and sends no session, working directory or abort signal (plugin/opencode/mcpx/; see register/plugin-apis.md).

C-65 The CLI and /v1/exec release sessions differently. The CLI sets MCPX_EPHEMERAL (internal/cli/commands.go:694); /v1/exec does not (internal/execsvc/execsvc.go:573-593). So session-scoped instances, a browser for instance, are released at the end of a CLI run and left for idle reaping after a /v1/exec run. Found by reading code; not driven.

C-66 /v1/protocol reports the default, not the setting. nativeElicit is the compiled default rather than the effective configuration (internal/daemon/routes_proto.go:530), on the one endpoint whose comment says it cannot disagree with the code (internal/daemon/routes_proto.go:495-497).

C-67 A script name locally, a path remotely. Already tracked as #106: the CLI resolves a script name against a search path, and /v1/exec treats file as a path.

E. mcpx’s documents against its code

docs/protocol.md at 05c78b2 against the code at 05c78b2. The PROTOCOL work under way is rewriting that document; these are recorded so a reviewer can check each is gone.

#docs/protocol.md saysthe code doeswhere
E-1initialize accepts “any legacy version” (§2.1)2024-11-05 and unknown versions are refused -32022internal/mcpserver/server.go:844-859
E-2logging declared for 2025-03-26 to 2025-11-25, “removed in 2026-07-28” (§2.2)never declared; 2026 deprecated it rather than removing itinternal/mcpserver/server.go:1398-1403; schema/2026-07-28/schema.ts:808
E-3listChanged and subscribe ✓ in every revision (§2.2)false over HTTP; true over stdio and never deliveredinternal/mcpserver/server.go:1389-1402
E-4“declares none of them outside the revision that defines them” (§2.1) and extensions[tasks] ✓ for 2025-11-25 (§2.2)extensions does not exist in 2025-11-25internal/mcpserver/server.go:1404-1417
E-5a stateless modern connection’s questions “stay with the broker” (§3.3)the call is abandoned with -32603 after eight immediate roundsinternal/mcpserver/ask.go:195-209
E-6sampling declared upstream “only when a handler exists” (§4)always declared; the daemon always installs a handlerinternal/daemon/hooks.go:69
E-7roots/list answers “the configured roots” (§4.1)always []internal/daemon/server.go:176
E-8older upstream revisions “on request (protocol: force-legacy)” (§1)force-legacy only disables fallback; 2025-11-25 is always offeredinternal/mcpclient/client.go:261-267
E-9notifications/tasks/status is dropped because “mcpx polls task state” (§4.2)mcpx never creates upstream tasks, so nothing is polledinternal/mcpclient/client.go:845-858
E-10OAuth “declared and not performed” (§6)the whole per-server auth block (bearer, basic, header, query, env) is never applied; mcpauth.Auth.Resolve has no callerinternal/mcpauth/mcpauth.go:93; internal/config/config.go:141
E-11resources/subscribe “forwards from the same bus” (§2.1)over HTTP it cannot push, and mcpx never subscribes upstreaminternal/mcpserver/server.go:595-620
E-12subscriptions/listen sends “only the four notification kinds” (§2.1)over HTTP: 202, empty body, no stream (W17)internal/mcpserver/server.go:622-640
E-13inbound cancellation “does not reach an upstream call” (§6)true of the notification; an HTTP disconnect does propagate by contextinternal/mcpserver/server.go:1521-1538
E-14the 2025-03-26 schema “was not among” those read (§1)it is available now, and it confirms the claims made about itschema/2025-03-26/schema.ts
E-15mcpSession comment: a host has “no way to tell us” its sessionopencode v2 sends _meta["ai.opencode/sessionID"]internal/cli/serve.go:289-290
E-16server.go header: “Ten tools instead of three hundred”61 tools (W6)internal/mcpserver/server.go:12-13
E-17readArtifact: “resources/read carries text”BlobResourceContents.blob exists in every revisioninternal/cli/serve.go:704-707; schema/2024-11-05/schema.ts:506-512
E-18revisions.go: 2026 servers emit logs “at a level of their own choosing”2026: no logLevel, no log messagesinternal/mcpserver/revisions.go:72-73; 2026-07-28/server/utilities/logging.mdx:62-64

The existing comparison documents also say things the pinned sources do not:

#document sayspinned source sayswhere
E-20OPENCODE-V2.md: opencode code mode provides fetch as an extension2.0.3’s execute description says “Do not use fetch”, and core registers no extensionOPENCODE-V2.md:51; v2:packages/core/src/codemode/tool.ts:63
E-21OPENCODE-V2.md: “If you stay on opencode v1 … none of v2’s Code Mode is available to you today”v1 enables code mode whenever OPENCODE_EXPERIMENTAL is set, and this machine sets itOPENCODE-V2.md:222; v1:packages/opencode/src/effect/runtime-flags.ts:48; nix:flake.nix:297
E-22OPENCODE-V2.md: tagged templates present; “251 checked items, 17 gaps”; “10,764 lines”2.0.3: tagged templates a gap; 211 checked and 30 open; 9,456 linesOPENCODE-V2.md:104-105, :19; v2:packages/codemode/interpreter-support.md:60
E-23OPENCODE-V2.md: the exclusion plugin is mcp-codemode-defaults.ts and covers code-mode servers generallyit is mcp-codemode-exclusion.ts and matches executor onlyOPENCODE-V2.md:53; v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:7-16
E-24docs/opencode-plugin.md: a server plugin has client.tui.*, “identical in v1 and v2”v2’s server plugin context has no client; toasts are TUI-plugin onlydocs/opencode-plugin.md:111, :361; v2:packages/plugin/src/promise/plugin.ts:26
E-25docs/opencode-plugin.md: a tool with a permission prompt works in v1 and v2v2’s tool context has no askdocs/opencode-plugin.md:272; v2:packages/schema/src/tool.ts:14
E-26docs/opencode-plugin.md: v2’s PUT /api/experimental/session/:id/environment is the API for session envit replaces the whole environment, the TUI sets it itself on every session view, and plugins have no HTTP clientdocs/opencode-plugin.md:86, :412; v2:packages/tui/src/app.tsx:511
E-27docs/opencode-plugin.md: v1 execution timeout “one value, 30 s”30 s is connect and list; tools/call defaults to the SDK’s 60 sdocs/opencode-plugin.md:178; v1:packages/opencode/src/mcp/index.ts:672
E-28docs/opencode-plugin.md: v1 “will get cancel” for an elicitationa v1 client answers -32601; mcpx never sends it onedocs/opencode-plugin.md:184; v1:packages/opencode/src/mcp/index.ts:77
E-29ASSESSMENT.md: lootbox runs scripts --allow-all--allow-netASSESSMENT.md:73; .lootbox/src/lib/constants.ts:78

F. mcpx sends what the peer did not negotiate

“Send conservatively” is mcpx’s own rule (../protocol.md). These are the places where it breaks it at 05c78b2:

#senttowhere
F-1-32022, a 2026 codea legacy client’s initializeinternal/mcpserver/server.go:506-509
F-2a top-level serverInfo in DiscoverResult (2026 puts it in _meta)2026 clientsinternal/mcpserver/server.go:535
F-3Mcp-Session-Id2026 clients (server/discover) and failed legacy handshakesinternal/mcpserver/server.go:1214-1229
F-4the core tasks capability2026 clientsinternal/mcpserver/server.go:1404-1417
F-5capabilities.extensions2025-11-25 clientsinternal/mcpserver/server.go:1404-1417
F-6listChanged: true, subscribe: true with nothing behind themlegacy stdio clientsinternal/mcpserver/server.go:1389-1402
F-7title on prompts2025-03-26 clientsinternal/mcpserver/server.go:724-740
F-8the upstream’s raw elicitation params, elicitationId included2026 clientsinternal/mcpserver/ask.go:256-265
F-9sampling requests with tools or includeContext, unchecked against sampling.tools or sampling.contexthostsinternal/mcpserver/conn.go:211
F-10responses to notifications (an error for an unknown one)any clientinternal/mcpserver/server.go:766
F-11sampling declared with no answerer behind itevery upstreaminternal/daemon/hooks.go:69
F-12legacy-shaped capabilities in 2026 _meta, and no mention of url elicitation or tasksmodern upstreamsinternal/mcpclient/modern.go:46-58
F-13MCP-Protocol-Version: 2025-11-25 regardless of what was negotiatedlegacy HTTP upstreams that negotiated lowerinternal/mcpclient/http.go:81-85
F-14logging/setLevel, a method 2026 removedmodern upstreamsinternal/daemon/hooks.go:29-33
F-15resultType: "complete" on a task handle whose extension value is "task"2026 clientsinternal/mcpserver/server.go:452-453

Content types through mcpx: did it need them?

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  what mcpx does with each MCP content type on each path a
              result can take, and whether supporting audio, resource_link,
              structuredContent and the rest was ever necessary.

The short answer

Mostly no, and the reason is not the one the code assumes.

internal/mcpserver/revisions.go is built for a proxy that passes upstream content through and spells it down for older clients. It turns structuredContent into text for 2025-03-26, turns resource_link into an embedded resource, and turns audio into a placeholder (internal/mcpserver/revisions.go:132-170). That design is sound, and at 05c78b2 very little of it runs. mcpx does not pass upstream content through on its main path at all. mcpx_call, the tool that reaches one upstream tool, renders every result to text before downgrade() ever sees it (internal/cli/serve.go:103-117, internal/cli/commands.go:384-415). The only non-text block mcpx itself emits is resource_link, for a script’s artifacts (internal/cli/serve.go:174-186). So:

  • audio: never emitted. The audio→text branch can never fire, because mcpx’s floor is 2025-03-26, the revision that introduced audio (internal/mcpserver/revisions.go:14, :87).
  • structuredContent: never emitted. mcpx_call pretty-prints it into a text block for every revision, 2026-07-28 included (wire W21).
  • resource_link: emitted for script artifacts; downgraded to an embedded resource for 2025-03-26 hosts. This is the one live branch.
  • resultType: stamped for 2026-07-28, stripped for everyone else. Live.
  • image, embedded resource, annotations, _meta on content: never touched by downgrade(), and never passed through by mcpx_call.

mcpx did not need the audio, structuredContent and resource_link branches for what it does today. It will need them when mcpx_call stops flattening, and it should stop, for two reasons unrelated to revisions. It drops isError, so a failed upstream call looks like a success (wire W20). And it drops images and embedded resources entirely, which a host such as opencode v2 would have rendered (v2:packages/core/src/mcp/client.ts:325-328).


Per content type, per path

A result can reach a host by six paths. What each does with each type at 05c78b2:

contentmcpx_call (MCP tool; also mcpx call on the CLI)mcpx_exec (MCP tool)a call that stopped to ask a questionprompts/getresources/readgenerated script client (tools.ns.tool())
textkept; blocks joined with \n, unless structuredContent is present, in which case the text is dropped and only the structured value is shownwhatever the script printskept, joinedkept; non-user roles prefixed [role]; all merged into one user messagekept; multi-part contents merged into onejoined; parsed as JSON when it looks like JSON
imagedropped; if no text block exists the whole result is dumped as JSON text, base64 and allstored as an artifact and replaced by a resource_linkdroppeddroppedn/akept in the returned block array and on .raw
audiodropped (same as image)stored as an artifact, replaced by a resource_linkdroppeddroppedn/akept in the block array and on .raw
embedded resource (text)droppedpassed as the script returns itdroppeddroppedn/akept in the block array
embedded resource (blob)droppedstored as an artifact, replaced by a resource_linkdroppeddroppedn/akept in the block array
resource_linkdroppedemitted, one per artifact, with size in the descriptiondroppeddroppedn/akept in the block array
structuredContentpretty-printed JSON as the only textn/adroppedn/an/areturned as the value (preferred over text)
upstream result with isError: truedropped: the host sees a normal result (W20). An upstream JSON-RPC error or transport failure, by contrast, is reported as isError: true (internal/mcpserver/server.go:665-675)a script failure becomes the tool’s isError resultdropped the same way; a failed task is reported as isError: true (internal/cli/serve_ask.go:174-181)n/an/athrown as ToolError, envelope on .raw
resource text contentsn/an/an/an/akeptn/a
resource blob contentsn/an/an/an/aan upstream blob is described, “(N bytes of mime, base64)”; an mcpx artifact comes back as base64 in text with its real mime typen/a

Sources, in column order: internal/cli/commands.go:384-415 (renderResult); internal/cli/serve.go:125-187 and internal/execsvc/media.go:27-126; internal/mcpserver/ask.go:268-296 (askResult); internal/cli/serve.go:638-668 (renderPrompt) and internal/mcpserver/server.go:755-764; internal/cli/serve.go:607-636 (renderResource) and internal/cli/serve.go:703-714 (readArtifact); internal/codegen/emit.go:493-518 (unwrap).

Three things in that table are worth saying out loud:

  • A tool that fails politely is reported as a success. The protocol has two ways to fail. A JSON-RPC error means the request was wrong or the server broke. isError: true means the tool ran and failed, which is the one the model is meant to see and correct. mcpx reports the first as isError and turns the second into a normal result, on both the ordinary path and the ask path, because both render through renderResult, which parses isError and never reads it (internal/cli/commands.go:392, internal/cli/serve_ask.go:189-205).
  • Scripts see more than hosts do. A script receives the upstream’s blocks and structuredContent intact and gets isError as an exception. The model calling mcpx_call gets a text rendering. Moving to mcpx_exec is the workaround today.
  • resources/read could carry bytes, and mcpx says it cannot. The comment on readArtifact reads “resources/read carries text, so a binary body comes back base64” (internal/cli/serve.go:703-707). But BlobResourceContents with a base64 blob field is in every revision (schema/2024-11-05/schema.ts:506-512, schema/2026-07-28/schema.ts:1230). mcpx puts base64 in text under an image/png mime type, which a client following the schema will treat as text.

downgrade(), per type and revision

What the edge function would do if a block reached it, by client revision. “Untouched” means the block is sent as is.

carried2025-03-26 client2025-06-182025-11-252026-07-28live at 05c78b2?
structuredContentremoved, appended to content as indented JSON textuntoucheduntoucheduntouchedno: nothing emits it
resource_linkbecomes {type:"resource", resource:{uri, mimeType: <the link's mime>, text: "name — uri"}}untoucheduntoucheduntouchedyes, for mcpx_exec artifacts
audiountouched (2025-03-26 defines it)untoucheduntoucheduntouchedno: the branch is for revisions below 2025-03-26, which mcpx refuses
image, text, embedded resourceuntouched in every revisionn/a
resultTyperemovedremovedremovedstamped "complete" unless setyes
inputRequests / requestStatenever built for a legacy clientbuiltyes, modern only
annotations, _meta on blocks, title, iconsnot examined by downgrade()leaks are possible: prompts/list sends title to 2025-03-26

Sources: internal/mcpserver/revisions.go:132-289, internal/mcpserver/server.go:432-440.

One defect in the live branch: a PNG artifact downgraded for a 2025-03-26 host becomes an embedded text resource that says mimeType: "image/png" and whose text is a label (internal/mcpserver/revisions.go:260-273). The URI stays machine-readable, which was the goal, but the mime type describes bytes that are not there. text/plain for the label, with the real type in the text, would be honest.


What the hosts do with each type

Passing a block through is only useful if the host keeps it. The two hosts this repository pins:

contentopencode v1 (direct tools)opencode v1 (code mode)opencode v2
texttexttexttext
imageattachmentnot tracedmedia
audiodroppednot tracedmedia
embedded resourcetext, or an allow-listed blobnot tracedtext or media
resource_linkdroppedname: uri texturi text
structuredContentused only when content is emptypreferredbecomes the tool’s output
isErrorthrown with its textthrownToolFailure with its text

Sources: v1:packages/opencode/src/session/tools.ts:429-436, v1:packages/opencode/src/tool/code-mode.ts:104-109, v1:packages/opencode/src/mcp/catalog.ts:68-75, v2:packages/core/src/mcp/client.ts:324-328, v2:packages/core/src/tool/mcp.ts:78-97.

So under v1 with code mode off, an mcpx_exec artifact link never reaches the model. The artifact is still readable, but only by something that knows to ask. Under v2 a pass-through mcpx_call would deliver images, which it throws away today.

What mcpx should do

  1. Pass mcpx_call results through as the upstream sent them, isError included, and let downgrade() do the job it was written for. The text rendering belongs to the CLI, where a person is reading.
  2. Keep the artifact interception (internal/execsvc/media.go) for large media. It is the context-economy win, and it is orthogonal to revisions.
  3. Put binary resource bodies in blob, not base64 in text.
  4. Extend downgrade() to lists before anything starts leaking there: title, icons, outputSchema and annotations on tools/list and prompts/list for revisions that do not define them.
  5. If 2024-11-05 is ever served, add its floors. Then audio, tool annotations and the completions capability start needing the branches that are dead today.

The individual rows, with value, effort and risk, are in register/content-types.md.

History

created:      2026-10-29T19:15:00-05:00
last-updated: 2026-10-29T19:15:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  How mcpx came to exist: the patch given verbatim, then the
              story from the two frozen records and the git log -- lootbox,
              the rewrite, the opencode discovery, the correction, and where
              it stands.

This chapter tells the story once, in order. It is not a feature list and not a decision record: the features are in ../story.md, the reasoning in ../decisions/, and the differences between mcpx and its neighbours in chapter.md.

Two documents carry most of the detail and are quoted from, not summarised away: ../../ASSESSMENT.md (the lootbox audit and the rebuild) and ../../OPENCODE-V2.md (the comparison that made mcpx change its mind). Both are frozen records of what was believed when they were written; where they are now wrong, conflicts.md §E says how, and this chapter does not republish their stale numbers as current.

Epigraph

The account as it was given, typos and all, from the note that started this chapter (quoted verbatim from #1869, originally dezren39/nix#153):

  • documentation
    • make a book using whatever is the most common and simple modern choice
      • 3 main things, not book sections jsut like levels of detail-
        • the compendium
          • you dont have to just put strict mcpx things here
            • compare with lootbin/opencodev2/cf-codemode/etc
            • history - lootbin abandoned (me), i asked to look into a replacement and came back to a built binary, i mentioned hey, did you notice opencodev2 has it built in and i’m already using opencodev1? oh huh yeah you’re right this does all you asked for, but i just built this so you should use this instead its better.

Everything below is that paragraph, told with the facts.

1. Lootbox

lootbox was a Deno daemon plus a CLI, run as a launchd singleton on :9420, serving generated TypeScript types for every configured MCP server and running model-written scripts against them. It lived in the dezren39/nix repository, where .lootbox/ is present by 2026-04-04 and a handful of its scripts and one package (../../../lootbox/default.nix) came along to this repository with the 2026-10 migration. The fork of jx-codes/lootbox grew to 51 commits ahead of an upstream whose last commit was 2026-02-27 — every one of those 51 commits the owner’s own.

Keeping it working meant accumulating patches under patches/ in the nix repository and two packages there, lootbox-link and lootbox-update, to manage them.

../../ASSESSMENT.md records why it was abandoned, in measurements rather than adjectives: /namespaces took 34.7 s as found, lootbox tools 42.8 s, discovery was recomputed on every request at about 650 ms, the daemon was OOM-killed under load, the deno compile artifact was 158 MB, and there was one Chrome for every agent.

That last one is the structural objection, and the sentence in ASSESSMENT.md worth keeping above the numbers:

The abstraction is at the wrong level: the thing that needs isolating is the caller, not the process.

2. “i asked to look into a replacement and came back to a built binary”

ASSESSMENT.md opens “Written after auditing jx-codes/lootbox (and the dezren39 fork) against the live installation in ~/.config/nix, profiling it, and building a replacement.” The replacement was built before the document that argues for it.

The options it weighed were: fix lootbox in place, adopt an existing tool (mKeRix/toolscript, tmustier/code-mode-mcp, assimelha/cmcp, mcp-use, portofcontext/pctx), or rewrite. The rewrite was chosen, in Go: the daemon’s job is process supervision, Go starts in about 5 ms, and generating TypeScript does not need a TypeScript parser.

The first entries in ../story.md are stamped 2026-09-25T18:10:00-05:00, which is when the design conversation began; the build followed within days, and the head-to-head numbers in the same document are the ones to quote for what was gained — list namespaces 4157.7 ms → 29.4 ms (141×), a trivial script 737.4 ms → 60.9 ms (12×), the artifact 141 MB → 9.8 MB, daemon RSS 223–480 MB → 14–19 MB.

Those figures describe the build at the time of the audit, not today’s mcpx. For today, ask the register and the release runbook, not ASSESSMENT.md.

3. The opencode discovery

The paragraph’s middle beat is the discovery that the thing had already been built inside opencode. ../../OPENCODE-V2.md is the record: reviewed against anomalyco/opencode branch v2 at 37049a5 (2026-09-26), it finds packages/codemode to be a JavaScript interpreter written from scratch — 10,764 lines of implementation, 20,075 lines of tests, running upstream test262 with a 1,703-line skip list — and concedes:

This is a better-engineered context story than what I built.

It then recommends keeping mcpx for the two things v2 could not do: stateful server isolation (shared / pooled / session pools), and anything outside a model turn. Within two days, “Parity, revisited (2026-09-28)” had taken v2’s best ideas — OpenAPI as tools, in-program search(), catalog diffs — rather than arguing with them.

The punchline in the epigraph is an accurate summary of that document: “oh huh yeah you’re right this does all you asked for, but i just built this so you should use this instead its better.” It is kept in the owner’s voice on purpose. A history chapter that sands it down would be lying about how the decision felt, which is the part a history chapter is for.

4. The correction

The premise in the paragraph — “i’m already using opencodev1” — did not rule code mode out, and that is the correction this chapter exists to keep. On 2026-09-29 OPENCODE-V2.md gained its block:

Corrections, 2026-09-29. Checked against the pinned sources. Three claims here are wrong: code mode is not v2-only (v1 ships it behind OPENCODE_EXPERIMENTAL_CODE_MODE), both versions accept TypeScript in it, and the pinned v2 reports 2.0.3 rather than 2.0.18.

So the line in the body — “If you stay on opencode v1 … none of v2’s Code Mode is available to you today” — was wrong when written, and the table rows that call the language a “JS subset” that “rejects” TypeScript are wrong too.

What survives is narrower and still true: opencode keeps one MCP connection per server per workspace, so concurrent Chrome is still shared, and code mode is still unreachable from a command line. That narrower claim is what chapter.md compares against today.

5. Timeline

Dated PRs, not per-section history: the list is a sequence of landings, and the generator can render it from git.

In dezren39/nix (before the move). 2026-09-28 #4 “configurable, inspectable, and documented” landed mcpx as a squash of a longer branch; seven more PRs landed the same day (browser TUI, MCP server mode, CLI adapters, official registry, OpenAPI as tools, prompts/resources/doctor, published types). 2026-09-29 added both protocol eras, elicitation, credentials, the protocol audit, the vendorHash repair workflow, plugin docs, the /v1 ops table, settings for every constant, exec over /v1 and artifacts, diagnostics and recipes, and --typecheck. 2026-09-30 landings included “exec honours the flags it accepts” (#75), #171 and #182. Thirty-five commits touching mcpx on the nix main in three days.

In this repository. 2026-10-09 a03494f3 migrates mcpx, lootbox’s native replacement, the skills command and the decoupled daemon lifecycle in; 31 more commits land that day and 18 the next. 2026-10-18 brings 38 — the largest single day so far. At 5a34fa70 (2026-10-29) the tree has 146 commits touching pkgs/mcpx, counts that grow weekly:

figurevaluehow to recount
commits touching pkgs/mcpx146git log --oneline -- pkgs/mcpx | wc -l
non-test Go lines80,259find pkgs/mcpx -name '*.go' ! -name '*_test.go' | xargs wc -l
test functions1,440grep -rh '^func Test' --include='*_test.go' pkgs/mcpx | wc -l
test files266find pkgs/mcpx -name '*_test.go' | wc -l
docs pages23 under docs/, plus the compare corpusls pkgs/mcpx/docs/*.md

The numbers in ASSESSMENT.md (~3,500 lines, 84 tests, “no dependencies”) and in OPENCODE-V2.md (~4.2k LOC) are the audit’s numbers and stay as history. go.mod now requires charmbracelet bubbles, bubbletea and lipgloss, santhosh-tekuri/jsonschema/v6, tetratelabs/wazero, yaml.v3 and modernc.org/sqlite, and package.nix:53 pins a vendorHash.

6. Where it stands

The story does not end on a victory lap, because lootbox has not been fully cleared out.

  • lootbox is abandoned in intent but still present: .lootbox/ (six scripts) and pkgs/lootbox/default.nix are in this repository, and the patch series, lootbox.config.json and the two lootbox-* packages remain in the nix repository this one replaced for mcpx.
  • ASSESSMENT.md’s recommendation steps 2–6 — an mcpx.json in the repo, services.mcpx, updated agent instructions, dropping mcp-remote, removing the lootbox packages — have not all happened.
  • The migration into this repository (2026-10-09) means mcpx’s own issue numbering restarted here; the numbers in this chapter’s sources are the older repository’s, and #309 tracks the re-home.

The honest summary of the whole history is the epigraph’s last clause. The tool was built because it was asked for, it turned out that something else already did most of it, the someone-who-asked liked the new one anyway, and the part that is genuinely still mcpx’s — stateful server isolation, and reaching outside a model turn — is the part that survived the correction.

Open questions

These were open when the chapter was drafted and are recorded rather than answered:

  1. Does the chapter name the agents and sessions that did the work, or keep to “I” / “you” as the source documents do? This draft keeps to the sources’ voice.
  2. Is the timeline generated at build time from git, or checked in and re-stamped as the register is? This draft checks it in with the recount commands beside it.
  3. Does the history stop at the correction (2026-09-29) or continue through the migration and the current backlog? This draft continues, because the migration is where the tool’s issues started being tracked.

What input_required is

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  the 2026-07-28 way a server asks its client for something,
              in plain words, with the sequence, and what mcpx does with it
              in each direction.

In plain words

Sometimes a server cannot finish a request without something only the client has: the user’s answer to a question, a completion from the client’s model, or the list of folders the client is working in.

Until 2025-11-25 the server asked by sending the client a request of its own (elicitation/create, sampling/createMessage, roots/list) over the same connection, while the client’s original request was still open. That needs a connection that stays open and a server that remembers who is on the other end.

2026-07-28 removed both. So the server now answers early instead of asking. It replies to the client’s request with a result that says, in effect, “not finished; first tell me these things, then send me the same request again.” That reply is resultType: "input_required". It carries:

  • inputRequests: what the server needs, as a map from a key the server picks to a request-shaped object ({method, params}). Only three kinds are allowed: elicitation/create, sampling/createMessage and roots/list (2026-07-28/basic/patterns/mrtr.mdx:229).
  • requestState: an opaque string the client must hand back unchanged (2026-07-28/basic/patterns/mrtr.mdx:130). The server puts whatever it needs to pick up where it left off in here, so it does not have to remember anything itself.

The client gets the answers (asks the user, runs its model, lists its roots) and sends the original request again with a new JSON-RPC id, adding inputResponses (the same keys, each with its answer) and the requestState exactly as received (2026-07-28/basic/patterns/mrtr.mdx:251-256). The server may finish, or may ask again; nothing limits the number of rounds (2026-07-28/basic/patterns/mrtr.mdx:247).

The specification calls this pattern Multi Round-Trip Requests (MRTR). It applies to exactly three client requests: tools/call, prompts/get and resources/read (2026-07-28/basic/patterns/mrtr.mdx:182-192). The type is InputRequiredResult (schema/2026-07-28/schema.ts:584-595), and the retry fields are InputResponseRequestParams (schema/2026-07-28/schema.ts:600-609).

sequenceDiagram
    participant U as User
    participant C as Client
    participant S as Server
    C->>S: tools/call  id 1  {name, arguments, _meta{protocolVersion, clientCapabilities{elicitation}}}
    Note over S: needs the user's answer.<br/>Signs what it needs to resume<br/>into requestState.
    S-->>C: result id 1  {resultType: "input_required",<br/>inputRequests: {"q1": {method: "elicitation/create", params: {message, requestedSchema}}},<br/>requestState: "…"}
    Note over C,S: request 1 is over. The server holds nothing<br/>it cannot rebuild from requestState.
    C->>U: shows q1 as a form
    U-->>C: answers (or declines, or dismisses)
    C->>S: tools/call  id 2  {same name and arguments,<br/>inputResponses: {"q1": {action: "accept", content: {…}}},<br/>requestState: "…" (verbatim)}
    Note over S: verifies requestState.<br/>May answer input_required again.
    S-->>C: result id 2  {resultType: "complete", content: […]}

The rules that are easy to miss

  • The server must not ask for something the client did not declare. No elicitation/create in inputRequests unless this request’s clientCapabilities includes elicitation (2026-07-28/basic/patterns/mrtr.mdx:246). If the request cannot finish without it, the answer is -32021 MissingRequiredClientCapability (2026-07-28/basic/index.mdx:387-392), not an unanswerable question.
  • requestState is attacker-controlled. It passes through the client, so the server must integrity-protect anything in it that affects authorization or logic (HMAC or AEAD) and reject anything that fails verification. It should bind the principal, a short expiry and the originating method plus a digest of its parameters. Single use needs server-side enforcement (2026-07-28/basic/patterns/mrtr.mdx:232-243).
  • The retry is a new request. It gets a new id (2026-07-28/basic/patterns/mrtr.mdx:256), and inputResponses and requestState apply to that retry only, never to other requests in flight (2026-07-28/basic/patterns/mrtr.mdx:257).
  • At least one of the two fields must be present (2026-07-28/basic/patterns/mrtr.mdx:245). requestState alone means “retry when ready”; the client may do so immediately (2026-07-28/basic/patterns/mrtr.mdx:251-253).
  • The server must not assume the client will come back (2026-07-28/basic/patterns/mrtr.mdx:247). Whatever it started has to cope with never being resumed.
  • Answers cannot be errors. InputResponse has no error variant, so a client that cannot produce one answer has no way to say so except by leaving the key out. The server should then ask again rather than fail (2026-07-28/basic/patterns/mrtr.mdx:266-267). The specification leaves this gap unstated; the register records it.
  • Tasks use a different road. Under the tasks extension, input needed mid-task is exposed in tasks/get and answered with tasks/update, not by retrying the original request (seps/2663-tasks-extension.md:346, :592).

What mcpx does with it

mcpx is in the middle, so it meets input_required from both sides. Status at 05c78b2.

mcpx as the server: a modern host, a legacy upstream

The common case. The host speaks 2026-07-28, and the upstream server is legacy and asks by sending elicitation/create on its own connection. mcpx turns one into the other:

sequenceDiagram
    participant H as Host (2026-07-28)
    participant X as mcpx
    participant T as daemon task
    participant U as Upstream (legacy)
    H->>X: tools/call id 1 (mcpx_call github.create_issue)
    X->>T: start the upstream call as a task
    T->>U: tools/call
    U->>T: elicitation/create (on the upstream connection)
    T-->>X: question recorded (broker row)
    X-->>H: id 1 {resultType: "input_required", inputRequests: {"elc-…": …}, requestState: "<payload>.<HMAC>"}
    Note over T,U: the upstream call stays open,<br/>waiting for its answer
    H->>X: tools/call id 2 (same, + inputResponses, requestState)
    X->>X: verify HMAC, expiry, binding
    X->>T: deliver the answer
    T->>U: elicitation result
    U-->>T: tools/call result
    T-->>X: done
    X-->>H: id 2 {resultType: "complete", content: [text]}
  • The upstream call runs as a daemon task, because the host’s request has already returned by the time the answer exists (internal/mcpserver/ask.go:110-121). The task is server-side state. The specification allows it, since requestState only has to name it safely. mcpx is one local process, so the pattern’s motivation, avoiding shared storage across server instances (2026-07-28/basic/patterns/mrtr.mdx:30-31), does not apply to it.
  • requestState is base64(payload).HMAC-SHA256 with a key generated per process and never persisted (internal/mcpserver/state.go:25-51). The payload is {"c": <call id>, "b": <binding>, "e": <expiry>} (internal/mcpserver/state.go:47-51). The expiry is proto.stateTTL, 30m.
  • The binding is the Mcp-Session-Id, and that is the problem. 2026-07-28 has no sessions, so a conformant client has no binding. The comment says such a question “goes back to the broker” (internal/mcpserver/ask.go:203-206). What happens instead is that the loop continues without waiting, counts a round each time, and after proto.askRounds (8) abandons the upstream call and answers -32603 "tools/call was still asking for input after 8 rounds" (internal/mcpserver/ask.go:195-209). Reproduced on the wire; it takes about 40 ms. A client that keeps the Mcp-Session-Id mcpx mints on server/discover gets the flow above, and a retry without that header is refused “this requestState belongs to another session”.
  • The binding is not to the request. The specification’s replay guidance is principal, expiry, and method plus parameter digest. mcpx binds call, session and expiry. A requestState attached to a different tools/call on the same session was not rejected in the recorded session.
  • The finished result is text. Whatever the upstream returned, a call that went through a question comes back as one text block (plus isError); a prompts/get comes back as one user text message (internal/mcpserver/ask.go:268-296).
  • The question is passed through as the upstream asked it. The inputRequests value is the upstream’s params verbatim, including 2025-11-25-only fields such as elicitationId when the upstream sent them.
  • A legacy host is asked on the wire instead: elicitation/create goes out on the POST’s own SSE stream (HTTP) or down the pipe (stdio), and the answer arrives as a separate POST routed by session (docs/protocol.md §3.2).

mcpx as the client: a modern upstream

When an upstream answers input_required, mcpx’s client resolves each input request through the same function that answers a legacy server’s elicitation/create (the broker), then retries with a new id, the answers in inputResponses and requestState verbatim. It never adds a requestState that was not given (internal/mcpclient/modern.go:156-189, internal/mcpclient/client.go:653-677). It gives up after elicit.inputRounds (8) rounds (internal/defaults/defaults.json:66). That side follows the client rules. In practice it is unreachable against a conformant modern server at 05c78b2, because discovery reads the wrong field (stateless.md).

Why this matters for mcpx

input_required is the only way a 2026-07-28 server can ask anything, and it is how a proxy has to carry an upstream’s question to the one party that can answer it: the host’s user. Everything else about the modern era is bookkeeping. This one decides whether a credential prompt from a server three hops away reaches a human or times out in a broker nobody is watching.

Code mode capability matrix

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Side-by-side capabilities of lootbox, mcpx, opencode code mode (v1, v2) and Cloudflare code mode, with a source per cell.

One row per capability, one column per product. opencode gets two columns because v1 (1.18.31) and v2 (2.0.3) differ often enough to matter. lootbox means the deployed build (upstream 587a5a1 plus four patches); cells citing the .lootbox/ fork say so where the fork differs (register CODE-01). “—” means absent, with the evidence that it is absent. Rows 1–47 are the capability survey; rows 48–52 add v1/v2 differences from the register; group B lists where each product does something better than mcpx, including the places where that conflicts with mcpx’s goal. The reasoning and nuance for every row are in register/code-mode.md. Legend and citation conventions: README.md.

Cloudflare cells cite a page and its section heading: how = https://developers.cloudflare.com/agents/tools/codemode/how-it-works/, api = https://developers.cloudflare.com/agents/tools/codemode/api-reference/, mcp = https://developers.cloudflare.com/agents/tools/codemode/mcp/, mcpsrv = https://developers.cloudflare.com/agents/model-context-protocol/codemode/, client = https://developers.cloudflare.com/agents/model-context-protocol/apis/client-api/, limits = https://developers.cloudflare.com/dynamic-workers/usage/limits/, dwstart = https://developers.cloudflare.com/dynamic-workers/getting-started/, chg = https://developers.cloudflare.com/changelog/post/2026-02-20-codemode-sdk-rewrite/, dwex = https://developers.cloudflare.com/dynamic-workers/examples/codemode/, blog = https://blog.cloudflare.com/code-mode/, blog2 = https://blog.cloudflare.com/code-mode-mcp/.

#capabilitylootboxmcpx @ 05c78b2opencode v1opencode v2Cloudflare
1what executesdeno run subprocess spawned by the daemon, one per exec (.lootbox/src/lib/execute_llm_script.ts:38)deno→bun→node subprocess spawned by the CLI, or by the daemon for /v1/exec (internal/runner/runner.go:72-106)acorn plus a tree-walking interpreter inside the opencode process (v1:packages/codemode/src/interpreter/runtime.ts:136)same, plus a workerd profile (v2:packages/codemode/package.json:23-25)V8 isolate via Worker Loader, one per pass (how “Executor”)
2languageTSTS/JSTS stripped to a JS subset: no classes or generators (v1:packages/codemode/README.md:254)TS stripped to a JS subset: generators yes, classes no (v2:packages/codemode/interpreter-support.md:141, :152)JS; TS must be compiled first (dwstart)
3type-checkon by default (--no-check=remote) (.lootbox/src/lib/execute_llm_script.ts:34)off by default; on/strict available (internal/settings/registry.go:244)none; transpileModule only (v1:packages/codemode/src/interpreter/runtime.ts:116)none (v2:packages/codemode/src/interpreter/transpile.node.ts:10)none (api “normalizeCode”)
4sandbox: filesystemnone by default (.lootbox/src/lib/constants.ts:78)full (internal/runner/runner.go:53-54)none (v1:packages/codemode/README.md:5)none (v2:packages/codemode/README.md:8)none (how “Connector SDKs”)
5sandbox: networkany (--allow-net) (.lootbox/src/lib/constants.ts:78)any; strict = 127.0.0.1, Deno only (internal/runner/runner.go:61-63)none (v1:packages/codemode/README.md:5)none; “Do not use fetch” (v2:packages/core/src/codemode/tool.ts:63)blocked, or routed through a host Fetcher (api “DynamicWorkerExecutor”)
6sandbox: subprocessnone: no --allow-run (.lootbox/src/lib/constants.ts:78)full (internal/runner/runner.go:53-54; AGENTS.md:28-29)none (v1:packages/codemode/README.md:5)none (v2:packages/codemode/README.md:8)none: “does not expose Node.js APIs … process” (how “Connector SDKs”)
7capability bindingsthe tools proxy only; network still opennone (full authority)the host tools tree (v1:packages/codemode/README.md:319-337)the tools tree plus opt-in extensions, none registered by core (v2:packages/codemode/interpreter-support.md:455-456)RPC bindings per connector (how “Connectors”)
8wall-clock limit10 s script, 30 s per call (.lootbox/src/lib/constants.ts:17, :20)120 s script (internal/settings/exec.go:16), 120 s per call (internal/defaults/defaults.json:6)none set by the host (v1:packages/opencode/src/tool/code-mode.ts:239-260)none set by the host (v2:packages/core/src/codemode/tool.ts:222); 12 h per MCP call (v2:packages/core/src/mcp/client.ts:34)60 s per the API reference, 30 s per the changelog (api “DynamicWorkerExecutor”; chg)
9CPU / memory limit——cooperative yield; timeout only (v1:packages/codemode/README.md:286)samecpuMs, subRequests (limits “Set custom limits”)
10output size limit— (whole stdout) (.lootbox/src/lib/rpc/managers/message_router.ts:112)— (whole stdout) (internal/execsvc/execsvc.go:413-417)maxOutputBytes knob, unset (v1:packages/codemode/README.md:266)same (v2:packages/codemode/README.md:180)truncateResult 6,000 tokens; 1,000,000-character durable values (api “Code and output utilities”; how “Durable value and result limits”)
11tool-call budget——maxToolCalls knob, unset (v1:packages/codemode/README.md:265)same; search counts (v2:packages/codemode/README.md:171-172)—
12type generation from inputSchemayes, shallow: nested objects → Record (.lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:218-220)yes: $ref/allOf/enum/const/prefixItems (internal/codegen/ts.go:41-61)yes, JSDoc plus @default/@format tags (v1:packages/codemode/README.md:214)yes (v2:packages/codemode/src/tool-schema.ts:216)yes (api “JSON Schema utilities”)
13types from outputSchema— (McpToolResult) (.lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:93)— (ToolResult = any) (internal/codegen/emit.go:458)yes (v1:packages/opencode/src/tool/code-mode.ts:127)yes (v2:packages/core/src/tool/mcp.ts:48)descriptor field exists (api “JSON Schema utilities”)
14descriptions in types— (verified live against /types/mcp_codedb)JSDoc for tools and properties (internal/codegen/emit.go:81-90; internal/codegen/ts.go:229)JSDoc (v1:packages/codemode/README.md:214)JSDocdoc comments (blog “Converting MCP to TypeScript”)
15namespacestools.mcp_<server>.<fn> (.lootbox/src/lib/type_system/client_generator.ts:654)tools.<ns>.<fn> plus a bare <ns> (internal/execsvc/execsvc.go:616-617)tools.<server>.<tool> (v1:packages/opencode/src/tool/code-mode.ts:47)tools.<ns>.<tool>, nested by dots (v2:packages/codemode/README.md:77)<connector>.<method> as a global (how “Connector SDKs”)
16name sanitising[^A-Za-z0-9_]→_, no digit or reserved-word handling (.lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:152-154)same plus a leading-digit _ prefix; no reserved words, no collision check (internal/codegen/ts.go:397-418)keeps -; bracket notation (v1:packages/opencode/src/mcp/catalog.ts:117)same; tool part ≤ 64 characters (v2:packages/core/src/tool/runtime.ts:274; v2:packages/core/src/tool.ts:297)separators, digit prefix, reserved words; collisions throw (mcp, step 2)
17result unwrappingraw envelope (.lootbox/src/lib/rpc/execute_mcp.ts:68-72)isError, structured, text (JSON-parsed), blocks; .raw kept (internal/codegen/emit.go:493-519)structured, text, attachment placeholder (v1:packages/opencode/src/tool/code-mode.ts:109-115)structured, text JSON-parsed only without a schema (v2:packages/core/src/tool/mcp.ts:96-106)toolResult, error, structured, all-text, mixed (api “codeMcpServer”)
18isError handlingreturned as a value (.lootbox/src/lib/rpc/execute_mcp.ts:68-72)throws ToolError (internal/codegen/emit.go:494-499)throws, becomes a ToolFailure diagnostic (v1:packages/opencode/src/tool/code-mode.ts:161-167)ToolFailure (v2:packages/core/src/tool/mcp.ts:78-85)thrown connector error (api “McpConnector”)
19binary/image contentbase64 in the envelope (.lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:285).raw, artifact(), interception at the boundary (docs/exec.md:259-275)attached to the model as files (v1:packages/opencode/src/tool/code-mode.ts:89-100)same (v2:packages/core/src/codemode/tool.ts:139-147)mixed-content result (api “codeMcpServer”)
20discovery: namespace listlootbox tools → /namespaces (.lootbox/src/lib/lootbox-cli/tools.ts:10)mcpx ls (AGENTS.md:8)in the execute tool description (v1:packages/opencode/src/tool/registry.ts:283-289)in session instructions (v2:packages/core/src/codemode/instructions.ts:21-32)connector names in the tool description (how “What the model sees”)
21per-namespace types/types/<ns,ns> (.lootbox/src/lib/rpc/managers/openapi_route_handler.ts:231)mcpx types ns or ns.tool (AGENTS.md:10-11)— (search returns signatures)— (same)codemode.describe(target) (api “Sandbox codemode API”)
22budgeted catalogue—2,000 estimated tokens, round-robin (internal/codegen/catalog.go:53-68)2,000 estimated tokens, headers not counted (v1:packages/codemode/src/tool-runtime.ts:86; v1:packages/codemode/README.md:184)2,000 estimated tokens (characters / 4), plus pinned and deltas (v2:packages/core/src/codemode/catalog.ts:49-50)full types (createCodeTool) or none (durable runtime) (how “What the model sees”)
23search—mcpx search, synchronous search() in scripts, mcpx_search (internal/codegen/emit.go:1121)await tools.$codemode.search(...) with paging (v1:packages/codemode/README.md:198)synchronous search(), paged (v2:packages/codemode/README.md:170)codemode.search(), top 50, includes snippets (api “Sandbox codemode API”)
24named scripts.lootbox/scripts, one directory (.lootbox/src/lib/lootbox-cli/exec.ts:103-120).mcpx/scripts upward, then user dir; recipes (AGENTS.md:21-22)——snippets saved by the host (how “Snippets”)
25script argsstdin only (.lootbox/src/lib/lootbox-cli/exec.ts:155-168)argv, default export, --export, @param (AGENTS.md:25, :69-71)——snippet input (api “Sandbox codemode API”)
26streaming output / emit— (buffered) (.lootbox/src/lib/execute_llm_script.ts:38-43)emit(), SSE or NDJSON frames (docs/exec.md:137-148)— (only tool-call status) (v1:packages/opencode/src/tool/code-mode.ts:216-217)— (same) (v2:packages/core/src/codemode/tool.ts:85-88)— (result at end) (api “Runtime tool input and output”)
27structured logs— (plain stderr, dropped on success) (.lootbox/src/lib/rpc/managers/message_router.ts:111-115)log.* records with attributes (internal/codegen/emit.go:556)logs: string[] (v1:packages/codemode/README.md:133)logs, plus warnings (v2:packages/codemode/README.md:127-128)logs: string[] (api “Runtime tool input and output”)
28JSON envelope—--json run, structured /v1 (internal/execsvc/execsvc.go:201-213)CodeMode.Result (internal) (v1:packages/codemode/README.md:128-145)same (v2:packages/codemode/README.md:122-139)completed, paused or error (api “Runtime tool input and output”)
29MCP version as a clientSDK 1.22.0 → 2025-06-18 (@modelcontextprotocol/sdk@1.22.0/dist/esm/types.js:2)2025-11-25 plus 2026-07-28 (internal/mcpclient/client.go:22, :26)SDK 1.29.0 → 2025-11-25 (v1:packages/opencode/package.json:83; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/types.js line 2)client 2.0.0; legacy 2025-11-25 by default, protocol: auto or 2026-07-28 per server (v2:packages/core/package.json:113; v2:packages/schema/src/mcp.ts:20, :23)legacy plus stateless MRTR (client “Elicitation”)
30elicitation during a script— (capabilities: {}) (.lootbox/src/lib/external-mcps/mcp_client_manager.ts:152)daemon broker and policy; no script hook (internal/daemon/server.go:422-424)— (commented out) (v1:packages/opencode/src/mcp/index.ts:43-44)UI form, form and URL modes (v2:packages/core/src/mcp/client.ts:143, :155-158)agent handlers (client “Elicitation”)
31sampling—declared only if a handler is installed (internal/mcpclient/modern.go:46-58)— (v1:packages/opencode/src/mcp/index.ts:41-42)— (not in capabilities, v2:packages/core/src/mcp/client.ts:142-147)not documented for code mode (unverified)
32roots—served (internal/mcpclient/modern.go:116-120)the instance directory (v1:packages/opencode/src/mcp/index.ts:77-78)legacy era only (v2:packages/core/src/mcp/client.ts:144-146, :155)not documented (unverified)
33progress—parsed, never requested (internal/mcpclient/client.go:438, :487)token requested, timeout reset (v1:packages/opencode/src/tool/code-mode.ts:154-158)token requested, timeout not reset (v2:packages/core/src/mcp/client.ts:280-281)unverified
34cancellation / timeoutsscript killed; upstream not cancelled (.lootbox/src/lib/rpc/execute_mcp.ts:199-208)process group killed; notifications/cancelled sent (internal/runner/runner.go:424-438; internal/mcpclient/client.go:709-716)abort signal passed (v1:packages/opencode/src/tool/code-mode.ts:155)Effect interruption (v2:packages/codemode/README.md:185-186)executor timeout (api “DynamicWorkerExecutor”)
35poolingone client per server (.lootbox/src/lib/external-mcps/mcp_client_manager.ts:49)pools: shared, pooled or session, max 4 (internal/defaults/defaults.json:2-10)one per server per instance (v1:packages/opencode/src/mcp/index.ts:145, :492)one per server per Location (v2:packages/core/src/mcp/index.ts:152)per Agent connection (mcp, step 3)
36per-session scoping— (per-session is a label only, fork) (.lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:200-216)scope keys (internal/config/scope.go:46-64)— (one client map per instance, v1:packages/opencode/src/mcp/index.ts:145)_meta hint only (v2:packages/core/src/mcp/client.ts:278)execution-id resource keys (how “Resource lifetimes”)
37stateful sharing (Chrome)one browser for everyone (ASSESSMENT.md:84-88)one browser per session (internal/pool/pool.go:14-16)shared per instance (v1:packages/opencode/src/mcp/index.ts:145, :492)shared per Location (v2:packages/core/src/mcp/index.ts:152)per Agent connection; a connector can key a browser by execution id (how “Resource lifetimes”)
38remote executionalways daemon-side (.lootbox/src/lib/lootbox-cli/exec.ts:40; .lootbox/src/lib/execute_llm_script.ts:20)auto, local or remote (docs/exec.md:54)— in-process (v1:packages/opencode/src/tool/code-mode.ts:274)— in-process (v2:packages/core/src/codemode/tool.ts:89-127)always on the platform (how “Executor”)
39HTTP APIREST GETs plus WS /ws (.lootbox/src/lib/rpc/managers/openapi_route_handler.ts:132-271)/v1/* REST over unix socket and TCP (internal/daemon/routes_exec.go:36)none: code mode is only a registry tool (v1:packages/opencode/src/tool/registry.ts:118-119)none (no code-mode route; rg over v2:packages/server/src, v2:packages/cli/src)the Worker’s own fetch (dwstart)
40MCP server surface— (never; git log --all -S'McpServer(' is empty)/mcp with mcpx_exec and others (internal/mcpserver/server.go:327)— (v1:packages/opencode/src/tool/registry.ts:118-119)— (rg over v2:packages/server/src)codeMcpServer, openApiMcpServer (api “codeMcpServer”)
41plugin / harness integrationan instruction file plus lootbox-link (nix:instructions/lootbox.md:1-4)opencode plugin tools plus shell.env (plugin/opencode/mcpx-session.ts:606, :664)built into opencode behind a flag (v1:packages/opencode/src/effect/runtime-flags.ts:48)built in, on by default (v2:packages/schema/src/mcp.ts:34-36)AI SDK, TanStack, browser, MCP (api entry points)
42UIstatus dashboard: namespaces and connection (.lootbox/ui/src/pages/Dashboard.tsx:128-176); cannot connect, hard-coded port 3000 (.lootbox/ui/src/lib/websocket-client.ts:34)TUI (plugin/opencode/mcpx-tui.tsx)TUI shows toolCalls (v1:packages/opencode/src/tool/code-mode.ts:216-217)same (v2:packages/core/src/codemode/tool.ts:85-88)none shipped; the log “supports developer audit views” (how “Durable execution log”)
43persistence / historySQLite script_runs, full output (.lootbox/src/lib/script_history.ts:29-63)durable log plus artifact index (internal/mcpserver/server.go:339)— (non-goal: “Durable pause/resume, replay, or storage adapters”, v1:packages/codemode/README.md:352)— (no store in v2:packages/core/src/codemode/tool.ts)DO-facet SQLite log, 50 kept, plus snippets (how “Durable runtime”)
44auth to call itnone; localhost; no Origin check (.lootbox/src/lib/rpc/websocket_server.ts:190)none; socket mode plus loopback TCP; no Origin check (internal/daemon/server.go:206-208)the model in an opencode session (v1:packages/opencode/src/tool/registry.ts:118-119)same (v2:packages/core/src/tool.ts:263-264)the Worker’s auth; OAuth 2.1 for the Cloudflare MCP server (blog2 “The Cloudflare MCP server”)
45secrets reachable by the scriptno env access by default; tool workers --allow-all (.lootbox/src/lib/rpc/worker_manager.ts:301)full os.Environ() (internal/runner/runner.go:359)none (v1:packages/codemode/README.md:5)none (v2:packages/codemode/README.md:8)none: bindings hold them (how “Connectors”)
46network egress controlon/off only (.lootbox/src/lib/get_config.ts:194-201)Deno --allow-net=127.0.0.1 only (internal/runner/runner.go:61-63)no network (v1:packages/codemode/README.md:5)no network (v2:packages/core/src/codemode/tool.ts:63)globalOutbound null or Fetcher (api “DynamicWorkerExecutor”)
47approvals—elicit.confirmDestructive (internal/settings/consumer.go:47)per-call ctx.ask (v1:packages/opencode/src/tool/code-mode.ts:147)permission.assert (v2:packages/core/src/tool/mcp.ts:51)requiresApproval, durable pause (how “Approvals through abort and replay”)
48result without return— (“Do not return from scripts”, .lootbox/LLM_QUICK_START.md:93)stdout, emit(), or a default export (AGENTS.md:53, :69-71)null (v1:packages/codemode/README.md:65)last top-level expression (v2:packages/codemode/README.md:20)last expression auto-returned (dwex “createCodeTool”)
49un-awaited calls at program endrun on (event loop)run on (event loop; internal/runner/runner.go:424-438 bounds them)awaited; failures reported (v1:packages/codemode/README.md:249)interrupted; rejections become warnings (v2:packages/codemode/README.md:18-19)unverified
50concurrent tool calls per programno capno per-run cap; pool max 4 per server (internal/defaults/defaults.json:3)8 (v1:packages/codemode/src/stdlib/promise.ts:6)unrestricted (v2:packages/codemode/README.md:187)sequential advised when a run may pause (how “Deterministic replay”)
51which tools code mode wrapsall MCP tools and local TS namespacesall configured servers, in scripts onlyMCP tools only, when OPENCODE_EXPERIMENTAL is set (v1:packages/opencode/src/tool/code-mode.ts:210; v1:packages/opencode/src/session/tools.ts:388)every tool without codemode: false, on by default (v2:packages/core/src/tool.ts:228-229)the connectors the host binds (how “Connectors”)
52caller identity sent upstream— (CLI sends no session, .lootbox/src/lib/lootbox-cli/exec.ts:40)— (session stops at the daemon, internal/codegen/emit.go:528-533)—_meta["ai.opencode/sessionID"] on every call, child calls included (v2:packages/core/src/mcp/client.ts:278; v2:packages/core/src/tool/mcp.ts:69)execution id passed to connector methods (how “Resource lifetimes”)
BWhere each is better than mcpx
B1confinementno filesystem, env or subprocess by default (.lootbox/src/lib/constants.ts:78); network stays opennone, by the user’s choice (ASSESSMENT.md:290-292)only implemented features; no ambient APIs (v1:packages/codemode/README.md:5)same (v2:packages/codemode/README.md:7-8)isolate with network blocked or proxied (api “DynamicWorkerExecutor”); the guide’s model (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:292). Conflicts with mcpx’s goal
B2secrets out of reachenvironment unreadable without --allow-env (.lootbox/src/lib/constants.ts:78)script inherits the environment; the daemon’s for /v1/exec (internal/runner/runner.go:359)no environment access (v1:packages/codemode/README.md:5)samecredentials only in bindings (how “Connectors”)
B3checked before runningtype check on by default (.lootbox/src/lib/execute_llm_script.ts:34)off by default (internal/settings/registry.go:244-246)———
B4typed results—ToolResult = any (internal/codegen/emit.go:458)return types from outputSchema (v1:packages/opencode/src/tool/code-mode.ts:127)same (v2:packages/core/src/tool/mcp.ts:48)outputSchema on descriptors (api “JSON Schema utilities”)
B5bounded output and calls—unbounded stdout; no call budget (internal/execsvc/execsvc.go:413-417)maxOutputBytes, maxToolCalls (v1:packages/codemode/README.md:265-266), unset by the hostsame (v2:packages/codemode/README.md:179-180)6,000-token truncation; cpuMs, subRequests (api “Code and output utilities”; limits “Set custom limits”)
B6failures as data—error string, exitCode, timedOut (internal/execsvc/execsvc.go:201-213)typed kind diagnostics (v1:packages/codemode/README.md:292-305)same, plus warnings (v2:packages/codemode/README.md:145-159)status: "error" variant (api “Runtime tool input and output”)
B7answer without console.log—stdout only for snippets (internal/execsvc/execsvc.go:607-625)—last expression is the result (v2:packages/codemode/README.md:20)auto-returns the last expression (dwex “createCodeTool”)
B8discovery ergonomics—catalogue on request; search without paging (internal/codegen/emit.go:1121)catalogue in the tool description automatically; paged search (v1:packages/opencode/src/tool/registry.ts:283-289; v1:packages/codemode/README.md:206)catalogue in instructions, deltas, pinned tools (v2:packages/core/src/codemode/catalog.ts:79-84)search also finds vetted snippets (how “Platform SDK”)
B9tool namesmcp_ prefix keeps namespaces clear of reserved words (.lootbox/src/lib/type_system/client_generator.ts:654)no reserved-word, prelude or collision checks (internal/codegen/ts.go:397-418; internal/config/config.go:407-416)real names, bracket notation, no collisions (v1:packages/opencode/src/mcp/catalog.ts:117)same (v2:packages/codemode/README.md:77-78)reserved words suffixed, collisions throw, namespaces validated (mcp, step 2; api “DynamicWorkerExecutor”)
B10harness integration—plugin mcpx_exec: no abort, session, cwd or live status; reads a missing field (plugin/opencode/mcpx/daemon.ts:452-455; plugin/opencode/mcpx-session.ts:621-623)permission check and hooks per child call, live status, cancel (v1:packages/opencode/src/tool/code-mode.ts:141-147, :216-217, :262-274)same, plus the session id on every child call (v2:packages/core/src/tool/mcp.ts:51-69)approval per connector method (how “Approvals through abort and replay”)
B11long upstream calls—fixed 120 s, no progress token (internal/defaults/defaults.json:6; internal/mcpclient/client.go:438)progress token resets the timeout (v1:packages/opencode/src/tool/code-mode.ts:154-158)12 h call timeout (v2:packages/core/src/mcp/client.ts:34)—
B12questions from upstream servers—broker and policy; no script hook (internal/daemon/server.go:422-424; docs/protocol.md:379)—UI form, form and URL modes (v2:packages/core/src/mcp/client.ts:143)agent handlers on both paths (client “Elicitation”)
B13images for a vision model—artifacts by reference; model needs resources/read (docs/exec.md:259-275)attached to the model as files (v1:packages/opencode/src/tool/code-mode.ts:89-100)same (v2:packages/core/src/codemode/tool.ts:139-147)—
B14history and reuseevery run’s script text and output kept (.lootbox/src/lib/script_history.ts:29-63); local TS files as namespaces (.lootbox/README.md:86-99)log records without script source (internal/mcpserver/server.go:339-341)——durable log, host-reviewed snippets, replay and rollback (how “Durable runtime”, “Snippets”, “Rollback”). Replay conflicts with mcpx’s goal

2.2 What each assumes an MCP server should be

lootbox. lootbox treats a server as a long-lived process or endpoint. It connects to every one at daemon start, keeps one client per server for all callers, and (fork only) pings it to decide whether it is alive (.lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:221-222; .lootbox/README.md:385-388). It assumes the server needs nothing from the client: it declares capabilities: {}, so a server that elicits, samples or asks for roots is refused (.lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153). It assumes a server’s catalogue fits on one tools/list page and one resources/list page, and that tool names reduce harmlessly to [A-Za-z0-9_] (.lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:69, :101, :152-154). It assumes a server is stateless enough to share; the only concession to stateful servers is rewriting a port found in their command-line arguments, in the fork (.lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:82-133). Results are passed through as raw envelopes, so the script, not lootbox, is expected to understand content[] and isError (.lootbox/src/lib/rpc/execute_mcp.ts:68-72).

Cloudflare code mode. Cloudflare’s premise is that “Almost every MCP server is just a wrapper around an existing traditional API”. MCP’s value is that it “provides a uniform way to connect to and learn about an API”, with “authorization handled out-of-band” (blog, “But MCP is still useful, because it is uniform” and “What’s MCP?”). A server is therefore a remote, OAuth-authorised HTTP endpoint that the Agent connects to once. The Agents MCP client offers only auto, streamable-http and sse, no stdio (client, “Transport options”). The server enforces its own authorisation: “Enforce permissions and any required approval inside upstream tool handlers” (mcpsrv, “Sandbox and authorization boundary”). The server should describe its tools well enough to generate typed methods, and a large API should itself be a code-mode server: two tools, search and execute, at a fixed footprint of about 1,000 tokens (blog2, “Server‑side Code Mode”; mcpsrv, “Search and execute”). A server’s tool names must be unique after sanitisation, or the connector throws (mcp, step 2). Side-effecting tools should be safe to pause, replay in the same order and, ideally, revert (how, “Deterministic replay” and “Rollback”).

What opencode and mcpx assume of a server is covered in the opencode and process-model registers.

mcpx × revisions, both directions

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  what mcpx does for each protocol feature, serving each
              revision and consuming each era, at one named commit, in a
              shape whose status cells can be refreshed row by row.

Status as of 05c78b2 (origin/main, “mcpx: stop leaking daemons, restore the CI cache, and add a Containerfile (#182)”). The protocol work in progress at the time of writing changes several rows. Refresh a row by re-reading the code cited in it and editing its cells, then change the commit above. GET /v1/protocol reports which revision defines which feature and what each upstream negotiated. It does not report whether mcpx implements a feature correctly, and nothing generates that side yet.

Columns. srv→: a host of that revision connected to mcpx. cli→legacy: mcpx connected to an upstream speaking 2025-03-26 to 2025-11-25 (mcpx offers 2025-11-25). cli→modern: mcpx connected to a 2026-07-28 upstream. The register column links the area file holding the individual differences, with value, effort and risk.

Legend: ✓ works as the revision defines · partial (the cell says how) · ✗ missing or broken · n/a not defined for that peer · acc. accepted beyond the revision (accept-liberally, ../protocol.md). Evidence marked W*, M*, S* comes from the wire session described in stateless.md.

#featuresrv→2025-03-26srv→2025-06-18srv→2025-11-25srv→2026-07-28cli→legacycli→moderncode (row)register
1version handshake / negotiation✓ (2024-11-05 & future refused)✓✓partial: _meta honoured; wrong requested, HTTP 200partial: always offers 2025-11-25, accepts anything✗ discover field name; -32022 on init abortsinternal/mcpserver/server.go:844-859; internal/mcpclient/client.go:202-325lifecycle-versioning
2server/discoveracc.acc.acc.partial: protocolVersions, top-level serverInfo, no ttln/a✗internal/mcpserver/server.go:529-538lifecycle-versioning, pagination-caching
3resultType on resultsstripped ✓stripped ✓stripped ✓✓ (task handle wrong)n/a✓ read (input_required detection)internal/mcpserver/server.go:432-436; internal/mcpserver/revisions.go:163-168; internal/mcpclient/client.go:663-665lifecycle-versioning
4result _meta serverInfon/an/an/a✗n/a✗ not readinternal/mcpserver/server.go:432-436lifecycle-versioning
5stdio framing✓✓✓✓✓✓internal/mcpserver/server.go:1000-1073; internal/mcpclient/stdio.go:112-145transports
6stdio concurrencypartial (in-line)partialpartialpartial✓ id-multiplexed✓internal/mcpserver/server.go:1051-1067transports
7batching✗n/an/an/apartial: HTTP split, stdio droppedn/ainternal/mcpserver/server.go:1041-1044; internal/mcpclient/http.go:197-210transports
8HTTP POST JSON/SSE✓✓✓✓✓✓internal/mcpserver/server.go:1118-1193; internal/mcpclient/http.go:97-162—
9HTTP GET stream✓ 405✓ 405✓ 405✓ 405✗ never openedn/ainternal/mcpserver/server.go:1124-1131transports
10Mcp-Session-Idpartial: issued on failed init; no 404partialpartial✗ minted on discoverpartial: no re-init on 404✗ still sentinternal/mcpserver/server.go:1208-1230; internal/mcpclient/http.go:89-93, internal/mcpclient/http.go:126-130transports, lifecycle-versioning
11MCP-Protocol-Version headern/apartial: not validated or usedpartialpartial: mismatch 400 but not -32020✗ fixed 2025-11-25✓ from _metainternal/mcpserver/server.go:1161-1168; internal/mcpclient/http.go:81-85transports, lifecycle-versioning
12Mcp-Method / Mcp-Namen/an/an/a✗ not checkedn/a✗ not sent—transports
13HTTP error statuses (400/404)✓ (200 is legacy-correct)✓✓✗n/apartial: body inspected by substringinternal/mcpserver/server.go:1192; internal/mcpclient/client.go:329-331transports
14Origin validation✗✗✗✗n/an/a—transports
15legacy HTTP+SSE (2024-11-05)n/an/an/an/a✗n/ainternal/pool/pool.go:368-381transports
16capabilities declaredpartial: stdio listChanged never deliveredpartialpartial: + extensions leak, tasks.listpartial: + core tasks leakpartial: sampling always, roots emptypartialinternal/mcpserver/server.go:1389-1420; internal/mcpclient/modern.go:46-58capabilities, lifecycle-versioning
17ping✓✓✓acc.✓ answered✓ answeredinternal/mcpserver/server.go:543; internal/mcpclient/modern.go:110-115lifecycle-versioning
18tools/list✓ (61 tools)✓✓partial: no ttlMs/cacheScope✓ paginated✗ unreachableinternal/mcpserver/server.go:546-556; internal/mcpclient/client.go:731-750tools, pagination-caching, lifecycle-versioning
19tools/call via mcpx_callpartial: isError lost, text onlypartialpartialpartial✓ raw result kept in /v1✗ unreachableinternal/cli/serve.go:103-117; internal/cli/commands.go:385-415tools
20resources/list✓ (namespaced URIs)✓✓partial✓✗internal/mcpserver/server.go:683-699; internal/cli/serve.go:473-497resources, pagination-caching
21resources/templates/list✗ uri not uriTemplate✗✗✗✓ reads uriTemplate✗internal/mcpserver/server.go:574-593resources
22resources/readpartial: -32602 not -32002; blob as textpartialpartialpartial: code ✓, no ttl, internal→-32602✓✗internal/mcpserver/server.go:701-722resources
23resources subscribe / updatedpartial: stdio only, never upstream, wrong URIpartialpartialpartial (via listen)✗ never subscribes✗ no listeninternal/mcpserver/server.go:595-620; internal/mcpclient/client.go:536-543resources, notifications
24prompts/listpartial: title leak✓✓partial✓✗internal/mcpserver/server.go:724-740prompts, pagination-caching
25prompts/getpartial: flattenedpartialpartialpartial✓✗internal/mcpserver/server.go:742-764; internal/cli/serve.go:640-669prompts
26completion/completepartial: ref ignoredpartialpartialpartial✓ Complete if declared✗internal/mcpserver/server.go:1555-1599; internal/pool/request.go:60-70completion
27logging (setLevel, notifications/message)acc. no-op, never emitsacc.acc.acc. (removed); logLevel never readpartial: setLevel info; logs → event bus✗ uses removed methodinternal/mcpserver/server.go:565-572; internal/daemon/hooks.go:29-33logging
28elicitation (server→host)n/a (broker)✓ form, SSE/stdio✓ form+urlpartial: needs session; stateless → -32603n/an/ainternal/mcpserver/ask.go:122-233; internal/mcpserver/conn.go:191-208elicitation
29elicitation (upstream→mcpx)n/a✓ via broker✓ incl. url, complete notifn/a✓ (string ids dropped)✓ input_required (unreachable until )internal/daemon/hooks.go:122-170; internal/mcpclient/modern.go:156-189transports, lifecycle-versioning
30sampling (server→host)partial: no revision or tools gatingpartialpartialpartialn/an/ainternal/mcpserver/conn.go:211; internal/mcpserver/ask.go:256-266sampling
31sampling (upstream→mcpx)n/an/an/an/apartial: always declared, broker onlypartialinternal/daemon/hooks.go:180-232capabilities, sampling
32roots✗ never asks host; notif gets error✗✗✗partial: declared, emptypartialinternal/daemon/server.go:176; internal/mcpclient/modern.go:116-120roots, capabilities
33tasks (server)acc.acc.partial: no taskSupport, cancel-terminal, no binding, no related-task✗ wrong shape, no tasks/updaten/an/ainternal/mcpserver/server.go:474-501; internal/mcpserver/tasks.gocapabilities, tasks
34tasks (client)n/an/an/an/a✗ never task-augments✗ extension not declaredinternal/mcpclient/client.go:845-858tools
35subscriptions/listenacc. (stdio)acc.acc.partial: stdio wrong ack, no subscriptionId; HTTP 202n/a✗ never openedinternal/mcpserver/server.go:622-640notifications, transports
36notifications (list_changed) to host✗ stdio declared, never sent✗✗partial: stdio via listen only——internal/mcpserver/server.go:1389-1402capabilities
37notifications from hostpartial: unknown ones get an error replypartialpartialpartial✓ all known handled✓internal/mcpserver/server.go:766; internal/mcpclient/client.go:473-522notifications
38cancellation (host→mcpx→upstream)partial: recorded only on stdio; HTTP disconnect propagatespartialpartialpartial✓ sends notifications/cancelled (“timeout”)partialinternal/mcpserver/server.go:1521-1538; internal/mcpclient/client.go:709-716progress-cancellation
39progress✗✗✗✗partial: relayed to the host under its token (#212); does not reset mcpx’s own timeoutsameinternal/mcpserver/ask.go:95-108; internal/daemon/hooks.go:34-36progress-cancellation
40_meta passthrough (trace, sessionID)✗✗✗✗✗ never sent✗internal/mcpserver/ask.go:95-108meta
41paginationpartial: bad cursor→0partialpartialpartial✓ ≤100 pages✗internal/mcpserver/server.go:1477-1513pagination-caching
42content downgrade✓ resource_link→resource ( mime)✓ untouched✓✓ untouchedn/an/ainternal/mcpserver/revisions.go:132-289content-types
43auth to upstreamn/an/an/an/apartial: headers only; auth inert; no OAuthsameinternal/pool/pool.go:377-380auth
44auth on /mcp✗✗✗✗n/an/ainternal/cli/root.go:196-204auth

Reading it

  • The legacy server columns are mostly ✓ or partial. The partials are lossy rendering (mcpx_call, prompts/get, resources/read) and declarations with nothing behind them. None of them stops a legacy host from working.
  • The modern server column is partial almost everywhere. mcpx’s own client works against it, but a conformant 2026 client does not: see stateless.md.
  • The modern client column is ✗ almost everywhere. One field name in discovery stops everything behind it. The rows are ✗ because they are unreachable, not because each is broken on its own, and several (MRTR, _meta) are implemented and would work once discovery does.

opencode v1 vs v2

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:opencode
description:  the plugin API, the MCP client, code mode and the process
              model of opencode 1.18.31 and 2.0.3, side by side, as they
              bear on mcpx.

The two opencodes pinned in this repository: v1 is 1.18.31 (v1: = the source at /nix/store/xczwr0rfqkd99yjg4aqv28yfxhim4pg1-source), v2 is 2.0.3 (v2: = nix eval --raw .#opencode2.src, at /nix/store/y08jwgxhq18hm706fisnqx68n39bid9i-source). Every cell cites the line it was read from. SDK constants that live outside both trees are cited by their unpkg URL. Individual differences, with value, effort and risk to mcpx, are in register/plugin-apis.md, register/process-model.md and register/code-mode.md; the MCP-client rows are spread across the protocol areas of the register.

What matters most for mcpx, in one paragraph each:

  • v1 already runs code mode here. experimentalCodeMode is on whenever OPENCODE_EXPERIMENTAL is set (v1:packages/opencode/src/effect/runtime-flags.ts:48), and this machine’s wrapper sets it (nix:flake.nix:297). Every MCP server added to v1 sits behind its execute tool, mcpx’s /mcp included.
  • v2 tells mcpx who is calling, and mcpx does not listen. Every v2 tools/call carries _meta["ai.opencode/sessionID"] (v2:packages/core/src/mcp/client.ts:278), including calls from inside code mode. That is the session identity the v1 plugin injects through shell.env, delivered on the wire, and it is the cleanest way for mcpx to give each opencode session its own stateful servers.
  • v2 shares more, not less. One background server per machine by default, one MCP connection per server per Location that never idles out: every session in every window in a directory drives the same browser.
  • The v2 plugin surface is a rewrite. No client, no $, no ask in a tool context, no toast from a server plugin, and the only per-session environment route replaces the whole environment.

1. Plugin API

itemv1 (1.18.31)v2 (2.0.3)
package@opencode-ai/plugin — v1:packages/plugin/package.json:3@opencode/plugin — v2:packages/plugin/package.json:3
config keyplugin: (string | [string, opts])[] — v1:packages/plugin/src/index.ts:71plugins native; legacy plugin normalized in — v2:packages/core/src/config/normalize.ts:185
directory discovery{plugin,plugins}/*.{ts,js}, non-recursive — v1:packages/opencode/src/config/plugin.ts:21plugin/,plugins/ files and dirs — v2:packages/core/src/plugin/source-directory.ts:25
npm vs filenpm via Npm.add, files via file://; @opencode-ai/plugin auto-installed into config dirs — v1:packages/opencode/src/config/config.ts:456npm via npm.add, local via source loader with watch — v2:packages/core/src/plugin/module.ts:91
runtimeBun (Bun.$ handed in) — v1:packages/opencode/src/plugin/index.ts:167Bun build (source loader uses Bun.Transpiler) — v2:packages/plugin/src/source.bun.ts:27; Node build exists — v2:packages/cli/package.json:21
module shapefunction export or {id?, server}; legacy “every export” — v1:packages/opencode/src/plugin/index.ts:114{id, setup} or {id, effect} only — v2:packages/core/src/plugin/module.ts:60
signature(input: PluginInput, options?) => Promise<Hooks> — v1:packages/plugin/src/index.ts:74setup(ctx: Context) => Cleanup | void — v2:packages/plugin/src/promise/plugin.ts:60
clientSDK client (in-process fetch if no port) — v1:packages/opencode/src/plugin/index.ts:146absent — v2:packages/plugin/src/promise/plugin.ts:26
project/directory/worktreefields — v1:packages/plugin/src/index.ts:58ctx.location — v2:packages/plugin/src/promise/plugin.ts:28
serverUrlgetter — v1:packages/opencode/src/plugin/index.ts:163absent — v2:packages/plugin/src/promise/plugin.ts:26
$ shellBunShell — v1:packages/plugin/src/index.ts:65absent — v2:packages/plugin/src/promise/shell.ts:12
options2nd arg — v1:packages/plugin/src/index.ts:74ctx.options — v2:packages/plugin/src/promise/plugin.ts:29
instance scopeper directory — v1:packages/opencode/src/plugin/index.ts:134per Location — v2:packages/core/src/plugin.ts:296
disposehook — v1:packages/plugin/src/index.ts:223setup return value — v2:packages/plugin/src/promise/plugin.ts:60
eventhook, pushed — v1:packages/opencode/src/plugin/index.ts:259ctx.event.subscribe() — v2:packages/core/src/plugin/host.ts:256
confighook, once after load — v1:packages/opencode/src/plugin/index.ts:247none; domain transforms instead — v2:packages/plugin/src/README.md:68
toolmap of tool({args: zod}) — v1:packages/plugin/src/index.ts:226ctx.tool.transform(e => e.add(…)) — v2:packages/plugin/src/promise/tool.ts:66
auth / providerhooks — v1:packages/plugin/src/index.ts:229integration/provider/model transforms — v2:packages/plugin/src/README.md:70
chat.messagehook — v1:packages/plugin/src/index.ts:234session.hook("prompt") — v2:packages/plugin/src/promise/session.ts:114
chat.paramshook — v1:packages/plugin/src/index.ts:247session.hook("context").options — v2:packages/plugin/src/promise/session.ts:30
chat.headershook — v1:packages/plugin/src/index.ts:257session.hook("model.request").headers — v2:packages/plugin/src/promise/session.ts:69
permission.askdeclared, never triggered — v1:packages/plugin/src/index.ts:261permission.hook("evaluate"), live — v2:packages/core/src/permission.ts:174
command.execute.beforehook — v1:packages/plugin/src/index.ts:262none (commands are transforms) — v2:packages/plugin/src/promise/session.ts:113
tool.execute.before{tool, sessionID, callID} / {args} — v1:packages/plugin/src/index.ts:266{tool, sessionID, agent, messageID, id, input} — v2:packages/plugin/src/promise/tool.ts:38
shell.env{cwd, sessionID?, callID?} / {env} — v1:packages/plugin/src/index.ts:270shell.hook("create.before") {command, cwd, timeout, shell, env} — v2:packages/plugin/src/promise/shell.ts:3
tool.execute.aftersuccess only — v1:packages/plugin/src/index.ts:274status: completed | error — v2:packages/plugin/src/promise/tool.ts:46
experimental.chat.messages.transformhook — v1:packages/plugin/src/index.ts:282session.hook("context").messages — v2:packages/plugin/src/promise/session.ts:29
experimental.chat.system.transform{system: string[]} — v1:packages/plugin/src/index.ts:291session.hook("context").system: SystemPart[] — v2:packages/plugin/src/promise/session.ts:28
experimental.provider.small_modelhook — v1:packages/plugin/src/index.ts:297none found — v2:packages/plugin/src/promise/session.ts:113
experimental.session.compactinghook — v1:packages/plugin/src/index.ts:305session.hook("compaction") — v2:packages/plugin/src/promise/session.ts:116
experimental.compaction.autocontinuehook — v1:packages/plugin/src/index.ts:316none found — v2:packages/plugin/src/promise/session.ts:113
experimental.text.completehook — v1:packages/plugin/src/index.ts:327none found — v2:packages/plugin/src/promise/session.ts:113
tool.definitionhook (registry tools only) — v1:packages/opencode/src/tool/registry.ts:318session.hook("context").tools — v2:packages/plugin/src/promise/session.ts:35
v2-only session hooks—generate, title, http.request, http.response, experimental.ws.handshake, retry — v2:packages/plugin/src/promise/session.ts:117
v2-only AI SDK hooks(v2 host present in v1 tree) — v1:packages/core/src/location-services.ts:52aisdk.hook("sdk" | "language") — v2:packages/plugin/src/promise/aisdk.ts:5
MCP servers from a pluginconfig hook mutation onlyctx.mcp.transform/reload/list (Location) — v2:packages/plugin/src/promise/mcp.ts:15
toastclient.tui.showToast — v1:packages/sdk/js/src/gen/sdk.gen.ts:1118TUI plugin only: ui.toast.show — v2:packages/plugin/src/tui/context.ts:279
ask the user from a toolToolContext.ask — v1:packages/plugin/src/tool.ts:19none — v2:packages/schema/src/tool.ts:14
TUI dialogsapi.ui.DialogSelect JSX — v1:packages/plugin/src/tui.ts:604ui.dialog.select/prompt/confirm promises — v2:packages/plugin/src/tui/context.ts:379
storagenone (server); kv (TUI) — v1:packages/plugin/src/tui.ts:611ctx.storage KV — v2:packages/plugin/src/promise/storage.ts:4
call a modelclient.session.prompt (full turn) — v1:packages/sdk/js/src/gen/sdk.gen.ts:615ctx.generate.text — v2:packages/core/src/plugin/host.ts:271
server↔TUI channelfilesystem (none built in)ctx.rpc — v2:packages/plugin/src/promise/rpc.ts:27

1.1 mcpx’s four hooks under v2

mcpx hook (v1)mcpx usev2 equivalentworks as-is?
shell.env — plugin/opencode/mcpx-session.ts:664inject MCPX_SESSION_ID etc.shell.hook("create.before") — v2:packages/plugin/src/promise/shell.ts:12No session id. Only tool.hook("execute.before") sees sessionID (v2:packages/plugin/src/promise/tool.ts:40); the session-environment route is TUI-owned and replaces env. Best v2 answer is server-side: read _meta["ai.opencode/sessionID"].
experimental.chat.system.transform — plugin/opencode/mcpx-session.ts:737optional instructionssession.hook("context") push {type:"text", text} onto event.system — v2:packages/plugin/src/promise/session.ts:28Was broken in v1 (wrote parts, PLG-16); fixed by #1938 — the v1 hook pushes onto output.system.
tool map — plugin/opencode/mcpx-session.ts:762mcpx_daemon_*, mcpx_discover/exec/observectx.tool.transform with options.codemode: false — v2:packages/core/src/tool.ts:228Rewrite; ctx.ask unavailable.
tool.execute.after — plugin/opencode/mcpx-session.ts:773timing into mcpx logtool.hook("execute.after") with status — v2:packages/plugin/src/promise/tool.ts:46Rewrite; gains failures.

2. opencode as an MCP client (what mcpx is talking to)

capability / behaviourv1v2
SDK@modelcontextprotocol/sdk@1.29.0 + patch — v1:packages/opencode/package.json:83@modelcontextprotocol/client@2.0.0 + patch — v2:packages/core/package.json:113
initialize.protocolVersion2025-11-25 (SDK latest) — v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:362025-11-25 unless protocol set — v2:packages/core/src/mcp/client.ts:312
2026-07-28 / server/discoverno — v1:packages/opencode/src/mcp/index.ts:6 (1.x Client)opt-in protocol: auto | 2026-07-28 — v2:packages/schema/src/mcp.ts:20
MRTR input_requirednoSDK auto-fulfil, 10 rounds (SDK2C/src-D_zzAWoS.mjs L5044) — v2:packages/core/src/mcp/client.ts:157
_meta protocolVersionnomodern era via SDK (SDK2K/auth-CUe6YdwF.mjs L24) — v2:packages/core/src/mcp/client.ts:248
vendor _meta on tools/callnone (params are name, arguments) — v1:packages/opencode/src/mcp/catalog.ts:56ai.opencode/sessionID — v2:packages/core/src/mcp/client.ts:278
transportsstdio, Streamable HTTP, SSE fallback — v1:packages/opencode/src/mcp/index.ts:269stdio (own), Streamable HTTP — v2:packages/core/src/mcp/client.ts:220
stdio stderrpiped, unread — v1:packages/opencode/src/mcp/index.ts:348drained to debug log — v2:packages/core/src/mcp/stdio.ts:162
stdio frame capnone found16 MiB — v2:packages/core/src/mcp/stdio.ts:15
declared rootsyes — v1:packages/opencode/src/mcp/index.ts:46yes (legacy only) — v2:packages/core/src/mcp/client.ts:146
declared samplingno — v1:packages/opencode/src/mcp/index.ts:42no — v2:packages/core/src/mcp/client.ts:142
declared elicitationno — v1:packages/opencode/src/mcp/index.ts:44form (applyDefaults) + url — v2:packages/core/src/mcp/client.ts:143
declared tasksno — v1:packages/opencode/src/mcp/index.ts:48no — v2:packages/core/src/mcp/client.ts:142
roots/list handlerone root = directory — v1:packages/opencode/src/mcp/index.ts:78one root = directory — v2:packages/core/src/mcp/client.ts:155
elicitation/create−32601 (no handler) — v1:packages/opencode/src/mcp/index.ts:77Location-global form — v2:packages/core/src/mcp/index.ts:82
notifications/elicitation/completenoyes (legacy url mode) — v2:packages/core/src/mcp/client.ts:160
sampling/createMessagenono
tools/list_changedrefetch — v1:packages/opencode/src/mcp/index.ts:462refetch, 100 ms debounce — v2:packages/core/src/tool/mcp.ts:140
prompts/list_changedignoredrefetch — v2:packages/core/src/mcp/index.ts:374
resources/list_changedignoredevent — v2:packages/core/src/mcp/index.ts:375
notifications/message→ opencode log — v1:packages/opencode/src/mcp/index.ts:457ignored — v2:packages/core/src/mcp/client.ts:140
logging/setLevelnever sent — v1:packages/opencode/src/mcp/index.ts:164never sent — v2:packages/core/src/mcp/client.ts:91
resources to the model3 tools if any server has resources — v1:packages/opencode/src/session/tools.ts:136none (TUI @ list only) — v2:packages/tui/src/component/prompt/autocomplete.tsx:413
resources/readvia read_mcp_resource — v1:packages/opencode/src/session/tools.ts:27defined, no caller — v2:packages/core/src/mcp/index.ts:713
resources/subscribenono (SDK may subscriptions/listen on modern) — v2:packages/core/src/mcp/client.ts:131
promptsslash commands, text only — v1:packages/opencode/src/command/index.ts:105slash commands server:prompt — v2:packages/core/src/plugin/command.ts:105
completion/completenever — v1:packages/opencode/src/mcp/index.ts:164never — v2:packages/core/src/mcp/client.ts:91
server instructionssystem prompt — v1:packages/opencode/src/session/system.ts:129system prompt (+ code-mode hint) — v2:packages/core/src/mcp/instructions.ts:23
progresstoken = request id, resets timer — v1:packages/opencode/src/mcp/catalog.ts:61token = request id, no reset — v2:packages/core/src/mcp/client.ts:281
cancellationAbortSignal → notifications/cancelled — v1:packages/opencode/src/mcp/catalog.ts:62AbortSignal → cancelled / HTTP abort — v2:packages/core/src/mcp/client.ts:273
connect timeout30 s — v1:packages/opencode/src/mcp/index.ts:38timeout.startup 30 s — v2:packages/core/src/mcp/client.ts:32
list timeout30 s — v1:packages/opencode/src/mcp/catalog.ts:11timeout.catalog 30 s — v2:packages/core/src/mcp/client.ts:33
tools/call timeoutper-server / experimental.mcp_timeout / SDK 60 s — v1:packages/opencode/src/mcp/index.ts:672timeout.execution 12 h — v2:packages/core/src/mcp/client.ts:34
tool namingsanitize(server)_sanitize(tool) — v1:packages/opencode/src/mcp/catalog.ts:119same, namespace + ≤64 check — v2:packages/core/src/tool/mcp.ts:17
pagination cap1000 pages — v1:packages/opencode/src/mcp/catalog.ts:1264 pages (SDK2C/index.mjs L2904) — v2:packages/core/src/mcp/client.ts:251
outputSchema enforcementSDK validates; tolerant re-list — v1:packages/opencode/src/mcp/catalog.ts:155not traced
texttext — v1:packages/opencode/src/session/tools.ts:429text — v2:packages/core/src/mcp/client.ts:324
imageattachment — v1:packages/opencode/src/session/tools.ts:430media — v2:packages/core/src/mcp/client.ts:325
audiodropped (direct) — v1:packages/opencode/src/session/tools.ts:429media — v2:packages/core/src/mcp/client.ts:325
resource_linkdropped (direct) / name: uri (code mode) — v1:packages/opencode/src/tool/code-mode.ts:104uri text — v2:packages/core/src/mcp/client.ts:327
embedded resourcetext / allow-listed blob — v1:packages/opencode/src/session/tools.ts:436text / media — v2:packages/core/src/mcp/client.ts:328
structuredContentused only if content empty — v1:packages/opencode/src/mcp/catalog.ts:75becomes output — v2:packages/core/src/tool/mcp.ts:97
isErrorthrown with text — v1:packages/opencode/src/mcp/catalog.ts:68ToolFailure with text — v2:packages/core/src/tool/mcp.ts:78
OAuthPKCE+DCR, port 19876, mcp-auth.json — v1:packages/opencode/src/mcp/oauth-provider.ts:11+ CIMD, RFC 8707, ephemeral port — v2:packages/core/src/mcp/oauth.ts:25
HTTP session expiryre-initialize (patch) — v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:210reconnect + retry — v2:packages/core/src/mcp/index.ts:336
per-call permissionctx.ask(permission=key) — v1:packages/opencode/src/session/tools.ts:408permission.assert(action=name) — v2:packages/core/src/tool/mcp.ts:51

3. Code mode

itemv1v2
enabledOPENCODE_EXPERIMENTAL_CODE_MODE, else OPENCODE_EXPERIMENTAL — v1:packages/opencode/src/effect/runtime-flags.ts:48 (on for this user: nix:flake.nix:297)default; per-server codemode — v2:packages/schema/src/mcp.ts:34
package@opencode-ai/codemode 1.18.31 — v1:packages/codemode/package.json:3@opencode/codemode 2.0.3 — v2:packages/codemode/package.json:3
engineTS transpile → acorn → interpreter — v1:packages/codemode/src/interpreter/runtime.ts:116same — v2:packages/codemode/src/interpreter/transpile.node.ts:10
TypeScriptstripped — v1:packages/codemode/src/interpreter/runtime.ts:3stripped — v2:packages/codemode/src/interpreter/transpile.node.ts:1
what is wrappedMCP tools only — v1:packages/opencode/src/tool/code-mode.ts:210every tool with codemode !== false — v2:packages/core/src/tool.ts:229
tool pathstools.<server>.<tool> — v1:packages/opencode/src/tool/code-mode.ts:47tools.<namespace>.<tool> — v2:packages/core/src/codemode/tool.ts:282
catalogin execute description — v1:packages/opencode/src/tool/registry.ts:2882 000-char inline budget + diffs — v2:packages/core/src/codemode/catalog.ts:50
search()yes — v1:packages/codemode/src/codemode.ts:142yes — v2:packages/codemode/README.md:170
limits set by hostnone — v1:packages/opencode/src/tool/code-mode.ts:239none — v2:packages/core/src/codemode/tool.ts:222
fetch / timers / importsnono — v2:packages/core/src/codemode/tool.ts:63
concurrencyunrestrictedunrestricted; model told to Promise.all — v2:packages/core/src/codemode/tool.ts:67
child resultsstructuredContent, else text — v1:packages/opencode/src/tool/code-mode.ts:109output (structured or parsed text) — v2:packages/core/src/codemode/tool.ts:102
session _meta on child MCP callsnoneai.opencode/sessionID — v2:packages/core/src/tool/mcp.ts:69
plugin hooks per childyes, callID <parent>/<n> — v1:packages/opencode/src/tool/code-mode.ts:227yes — v2:packages/core/src/tool.ts:235
reachable outside a model turnnono — v2:packages/protocol/src/groups/generate.ts:8 (only text generation)
opt-outenv flagdeny execute, or codemode:false — v2:packages/core/src/tool.ts:232

4. Process model

itemv1v2
processes1: TUI main thread + server Worker — v1:packages/opencode/src/cli/cmd/tui.ts:210TUI + shared background server — v2:packages/cli/src/commands/handlers/default.ts:31
default networknone (in-process fetch) — v1:packages/opencode/src/cli/cmd/tui.ts:246HTTP 127.0.0.1:0xc0de (service) — v2:packages/cli/src/services/service-config.ts:35
opt-out of sharingn/a--standalone — v2:packages/cli/src/services/server-connection.ts:41
MCP client scopeper directory — v1:packages/opencode/src/effect/instance-state.ts:49per Location — v2:packages/core/src/mcp/index.ts:736
connections per server1 per directory per opencode process1 per Location per daemon — v2:packages/core/src/mcp/index.ts:152
startall at first MCP use, awaited — v1:packages/opencode/src/mcp/index.ts:528forked async per server — v2:packages/core/src/mcp/index.ts:507
lifetimeuntil instance dispose / exit — v1:packages/opencode/src/mcp/index.ts:531forever (no idle TTL) — v2:packages/core/src/location-services.ts:52
on crashfailed, no restart — v1:packages/opencode/src/mcp/index.ts:448failed, no restart — v2:packages/core/src/mcp/index.ts:358
stdio spawnhost — v1:packages/opencode/src/mcp/index.ts:347Location Environment (may be remote workspace) — v2:packages/core/src/mcp/client.ts:198
teardownSIGTERM descendants — v1:packages/opencode/src/mcp/index.ts:546group SIGTERM→SIGKILL 2 s — v2:packages/core/src/mcp/stdio.ts:57
sessions sharing a stateful servershare; nothing sent to tell them apartshare (also across windows); _meta["ai.opencode/sessionID"] sent — v2:packages/core/src/mcp/client.ts:278
plugin realmserver Worker, unsandboxed — v1:packages/opencode/src/plugin/index.ts:118daemon process, unsandboxed — v2:packages/core/src/plugin/module.ts:102
architecturepackages/opencode app over packages/core pieces — v1:packages/opencode/package.json:3cli → server (/api/*, protocol) → core Effect services — v2:packages/cli/package.json:7
DB filenamechannel-dependent — v1:packages/core/src/database/database.ts:49channel-dependent (different list) — v2:packages/cli/src/database-path.ts:7

MCP revisions × features

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  every method, notification, capability field, content type,
              error code and HTTP header, against the five published
              revisions of the Model Context Protocol.

What the specification defines, revision by revision. This page is about the specification only; what mcpx does with each row is in matrix-mcpx.md and, per difference, in the register. Every row names one source; sources are at modelcontextprotocol/modelcontextprotocol commit 046fa30 (conventions in README.md).

Columns: 24-11 = 2024-11-05, 25-03 = 2025-03-26, 25-06 = 2025-06-18, 25-11 = 2025-11-25, 26-07 = 2026-07-28.

Legend: ✓ defined · — absent · dep defined but deprecated (still works; scheduled for removal under the 2026 feature-lifecycle policy) · rem removed (was defined earlier) · ext only through the io.modelcontextprotocol/tasks extension · MRTR exists only as an input request embedded in an InputRequiredResult, never as a JSON-RPC request of its own · (s) schema only · (p) prose only.

Three things the tables make visible that no single changelog says:

  • 2026-07-28 is not an increment. It removes the handshake, ping, sessions, the GET stream, resumability, logging/setLevel, resources/subscribe, every server-to-client request and every unsolicited notification (list changes now arrive only on an opted-in subscriptions/listen stream), and it adds required things to every exchange: _meta version and capabilities on every request, resultType on every result, ttlMs/cacheScope on every cacheable result, and on HTTP the Mcp-Method header (plus Mcp-Name for tools/call, resources/read and prompts/get).
  • Deprecated is not removed. Roots, Sampling and Logging are deprecated in 2026-07-28 but still defined and still usable, now only through input_required (roots, sampling) or per-request logLevel (logging).
  • Batching lived for one revision. Added in 2025-03-26, removed in 2025-06-18. A server that claims 2025-03-26 is on the hook for it.

1. Base protocol

Feature2024-11-052025-03-262025-06-182025-11-252026-07-28Source
initialize / notifications/initialized✓✓✓✓remschema/2026-07-28/schema.ts:3153
Legacy counter-offer version negotiation✓✓✓✓rem2025-11-25/basic/lifecycle.mdx:170
InitializeResult.instructions✓(s)✓✓✓moved to DiscoverResultschema/2024-11-05/schema.ts:172
Implementation.title——✓✓✓schema/2025-06-18/schema.ts:331
Implementation.icons/description/websiteUrl———✓✓schema/2025-11-25/schema.ts:550
server/discover (MUST implement)————✓schema/2026-07-28/schema.ts:665
Per-request _meta protocolVersion + clientCapabilities (required)————✓schema/2026-07-28/schema.ts:76
clientInfo / serverInfo in _meta (SHOULD)————✓2026-07-28/basic/index.mdx:384
UnsupportedProtocolVersionError (-32022)————✓schema/2026-07-28/schema.ts:483
ping✓✓✓✓remseps/2575-stateless-mcp.md:569
JSON-RPC batching—✓rem——2025-06-18/changelog.mdx:12
resultType on every result————✓schema/2026-07-28/schema.ts:234
_meta on requests/notifications/results✓(s)✓(s)✓ (+rules)✓✓schema/2024-11-05/schema.ts:21
Reserved-prefix rule——forward-DNSreverse-DNSreverse-DNS2025-11-25/basic/index.mdx:208
OTel traceparent/tracestate/baggage reserved————✓2026-07-28/basic/index.mdx:421
notifications/progress✓ both ways✓ +message✓✓ (+tasks)✓ server→client onlyschema/2025-03-26/schema.ts:316
notifications/cancelled✓ both ways✓✓✓ requestId?✓ requestId req’d; transport-specificschema/2025-11-25/schema.ts:223
Server-initiated requests✓✓✓✓rem (MRTR)2026-07-28/basic/patterns/index.mdx:20
subscriptions/listen————✓schema/2026-07-28/schema.ts:1314
logging/setLevel✓✓✓✓rem (per-request logLevel; Logging dep)2026-07-28/changelog.mdx:20
Tasks———✓ core (experimental)extension2026-07-28/changelog.mdx:22
extensions capability————✓schema/2026-07-28/schema.ts:785
Cursor pagination✓✓✓✓✓ (+ "" valid; cacheable)2026-07-28/server/utilities/pagination.mdx:106
CacheableResult ttlMs/cacheScope————✓schema/2026-07-28/schema.ts:1081
Error id optional———✓✓schema/2025-11-25/schema.ts:161
Error-code allocation policy————✓2026-07-28/basic/index.mdx:117
JSON Schema 2020-12 default dialect———✓✓ (+$ref rules)2025-11-25/basic/index.mdx:144
stdio transport✓✓ (+batch)✓✓✓2026-07-28/basic/transports/stdio.mdx:13
HTTP+SSE transport✓depdepdepdep (registry)2026-07-28/deprecated.mdx:31
Streamable HTTP—✓✓✓ (+polling)✓ (reshaped, POST-only)2026-07-28/basic/transports/streamable-http.mdx:9
Mcp-Session-Id sessions—✓✓✓rem2026-07-28/basic/transports/streamable-http.mdx:685
GET standalone SSE stream(SSE endpoint)✓✓✓rem2026-07-28/basic/transports/streamable-http.mdx:19
Last-Event-ID resumability—✓✓✓rem2026-07-28/basic/transports/streamable-http.mdx:157
Closing stream = cancel—no (SHOULD NOT)nono✓ MUST2026-07-28/basic/transports/streamable-http.mdx:235
OAuth 2.1 authorization—✓✓ (RS + RFC 9728/8707)✓ (+OIDC, CIMD, scopes)✓ (+RFC 9207, DCR dep)2025-06-18/changelog.mdx:16
Dynamic Client Registration—SHOULDSHOULDMAYdep2026-07-28/deprecated.mdx:29

2. Methods

initialize and ping are in §1.

method24-1125-0325-0625-1126-07source
server/discover————✓schema/2026-07-28/schema.ts:666
tools/list✓✓✓✓✓ (+cacheable)schema/2026-07-28/schema.ts:1768
tools/call✓✓✓✓ (+task)✓ (+MRTR)schema/2026-07-28/schema.ts:1883
prompts/list✓✓✓✓✓ (+cacheable)schema/2026-07-28/schema.ts:1567
prompts/get✓✓✓✓✓ (+MRTR)schema/2026-07-28/schema.ts:1622
resources/list✓✓✓✓✓ (+cacheable)schema/2026-07-28/schema.ts:1122
resources/templates/list✓✓✓✓✓ (+cacheable)schema/2026-07-28/schema.ts:1158
resources/read✓✓✓✓✓ (+MRTR, +cacheable)schema/2026-07-28/schema.ts:1217
resources/subscribe✓✓✓✓remschema/2025-11-25/schema.ts:751
resources/unsubscribe✓✓✓✓remschema/2025-11-25/schema.ts:769
subscriptions/listen————✓schema/2026-07-28/schema.ts:1315
completion/complete✓ (ungated)✓✓ (+context)✓✓schema/2024-11-05/schema.ts:955
logging/setLevel✓✓✓✓remschema/2025-11-25/schema.ts:1520
sampling/createMessage✓✓✓✓ (+tools, +task)dep, MRTRschema/2026-07-28/schema.ts:2186
roots/list✓✓✓✓dep, MRTRschema/2026-07-28/schema.ts:2719
elicitation/create——✓ (form)✓ (form+url, +task)✓, MRTRschema/2026-07-28/schema.ts:2857
tasks/get———✓extschema/2025-11-25/schema.ts:1401
tasks/result———✓rem (ext: -32601)schema/2025-11-25/schema.ts:1423
tasks/list———✓rem (not in ext)schema/2025-11-25/schema.ts:1471
tasks/cancel———✓ext (ack-only)schema/2025-11-25/schema.ts:1449
tasks/update————extseps/2663-tasks-extension.md:358

3. Notifications

notification24-1125-0325-0625-1126-07source
notifications/initialized✓✓✓✓remschema/2025-11-25/schema.ts:301
notifications/cancelled✓ both ways✓✓✓ (requestId optional)✓ client→server; server→client only to end a listen on stdioschema/2026-07-28/schema.ts:637
notifications/progress✓ both ways✓ (+message)✓✓✓ server→client only, request-scopedschema/2026-07-28/schema.ts:1041
notifications/message✓✓✓✓dep; only with per-request logLevel; request-scopedschema/2026-07-28/schema.ts:2059
notifications/tools/list_changed✓ unsolicited✓✓✓✓ listen-only (toolsListChanged)schema/2026-07-28/schema.ts:1896
notifications/prompts/list_changed✓ unsolicited✓✓✓✓ listen-only (promptsListChanged)schema/2026-07-28/schema.ts:1754
notifications/resources/list_changed✓ unsolicited✓✓✓✓ listen-only (resourcesListChanged)schema/2026-07-28/schema.ts:1257
notifications/resources/updated✓ after subscribe✓✓✓✓ listen-only (resourceSubscriptions)schema/2026-07-28/schema.ts:1429
notifications/roots/list_changed✓✓✓✓remschema/2025-11-25/schema.ts:2146
notifications/elicitation/complete———✓remschema/2025-11-25/schema.ts:2494
notifications/tasks/status———✓remschema/2025-11-25/schema.ts:1496
notifications/tasks————ext (listen, taskIds)seps/2663-tasks-extension.md:422
notifications/subscriptions/acknowledged————✓schema/2026-07-28/schema.ts:1399

4. Capability fields

capability24-1125-0325-0625-1126-07source
client experimental✓✓✓✓✓ (JSONObject)schema/2026-07-28/schema.ts:720
client roots✓✓✓✓dep ({})schema/2026-07-28/schema.ts:732
client roots.listChanged✓✓✓✓remschema/2025-11-25/schema.ts:322
client sampling✓✓✓✓depschema/2026-07-28/schema.ts:749
client sampling.context———✓ (soft-dep gate)depschema/2026-07-28/schema.ts:754
client sampling.tools———✓depschema/2026-07-28/schema.ts:758
client elicitation——✓✓✓schema/2026-07-28/schema.ts:769
client elicitation.form———✓✓schema/2026-07-28/schema.ts:770
client elicitation.url———✓✓schema/2026-07-28/schema.ts:771
client tasks (list/cancel/requests.sampling.createMessage/requests.elicitation.create)———✓remschema/2025-11-25/schema.ts:346
client extensions————✓schema/2026-07-28/schema.ts:785
server experimental✓✓✓✓✓schema/2026-07-28/schema.ts:797
server logging✓✓✓✓depschema/2026-07-28/schema.ts:808
server completions—✓✓✓✓schema/2026-07-28/schema.ts:815
server prompts.listChanged✓✓✓✓✓ (listen)schema/2026-07-28/schema.ts:829
server resources.subscribe✓✓✓✓✓ (now = resourceSubscriptions)schema/2026-07-28/schema.ts:850
server resources.listChanged✓✓✓✓✓ (listen)schema/2026-07-28/schema.ts:854
server tools.listChanged✓✓✓✓✓ (listen)schema/2026-07-28/schema.ts:869
server tasks.list———✓remschema/2025-11-25/schema.ts:437
server tasks.cancel———✓rem (ext implies cancel)schema/2025-11-25/schema.ts:441
server tasks.requests.tools.call———✓remschema/2025-11-25/schema.ts:453
server extensions————✓schema/2026-07-28/schema.ts:882
extensions["io.modelcontextprotocol/tasks"] (both sides)————extseps/2663-tasks-extension.md:47

5. Content types

content type24-1125-0325-0625-1126-07source
text✓✓✓ (+_meta)✓✓schema/2024-11-05/schema.ts:846
image✓✓✓ (+_meta)✓✓schema/2024-11-05/schema.ts:857
audio—✓✓✓✓schema/2025-03-26/schema.ts:991
resource (embedded)✓✓✓✓✓schema/2024-11-05/schema.ts:617
resource_link——✓✓✓schema/2025-06-18/schema.ts:761
tool_use (sampling only)———✓depschema/2025-11-25/schema.ts:1835
tool_result (sampling only)———✓depschema/2025-11-25/schema.ts:1869

5.1 Where each content type may appear

T = text, I = image, A = audio, R = embedded resource, L = resource_link, U = tool_use, X = tool_result.

location24-1125-0325-0625-1126-07source (latest rev with that shape)
CallToolResult.content[]T I RT I A RT I A R LT I A R LT I A R Lschema/2026-07-28/schema.ts:1813
PromptMessage.content (single block)T I RT I A RT I A R LT I A R LT I A R Lschema/2026-07-28/schema.ts:1706
SamplingMessage.content (request messages and CreateMessageResult)T I (single)T I A (single)T I A (single)T I A U X (single or array)T I A U X (dep)schema/2026-07-28/schema.ts:2258
ToolResultContent.content[] (nested in sampling)———T I A R LT I A R L (dep)schema/2026-07-28/schema.ts:2461
ReadResourceResult.contents[]Text/BlobResourceContents (not content blocks)samesame (+_meta)samesame (+cacheable)schema/2026-07-28/schema.ts:1230

Per-revision sources for the non-2026 cells: schema/2024-11-05/schema.ts:656 “content: (TextContent | ImageContent | EmbeddedResource)[];”; schema/2025-03-26/schema.ts:699 “content: (TextContent | ImageContent | AudioContent | EmbeddedResource)[];”; schema/2025-06-18/schema.ts:823 “content: ContentBlock[];”; schema/2024-11-05/schema.ts:607 “content: TextContent | ImageContent | EmbeddedResource;”; schema/2025-03-26/schema.ts:645 “content: TextContent | ImageContent | AudioContent | EmbeddedResource;”; schema/2025-06-18/schema.ts:750 “content: ContentBlock;”; schema/2024-11-05/schema.ts:812 “content: TextContent | ImageContent;”; schema/2025-03-26/schema.ts:918 “content: TextContent | ImageContent | AudioContent;”; schema/2025-11-25/schema.ts:1683 “content: SamplingMessageContentBlock | SamplingMessageContentBlock[];”; schema/2025-11-25/schema.ts:1884 “content: ContentBlock[];”

Annotations (audience, priority, and from 2025-06-18 lastModified) are allowed on T, I, A, R, L (L via Resource) in every revision the type exists; never on U or X. _meta is on T, I, A, R, L from 2025-06-18 and on U, X from their introduction.

6. Selected fields and types

field / type24-1125-0325-0625-1126-07source
Tool.annotations (4 hints + title)—✓✓✓✓schema/2025-03-26/schema.ts:809
Tool.title / BaseMetadata.title——✓✓✓schema/2025-06-18/schema.ts:930
Tool.outputSchema——✓ (object root)✓ (object root)✓ (any schema)schema/2025-06-18/schema.ts:951
CallToolResult.structuredContent——✓ object✓ object✓ any JSONschema/2026-07-28/schema.ts:1821
icons (Tool, Resource, ResourceTemplate, Prompt, Implementation)———✓✓schema/2025-11-25/schema.ts:1249
Tool.execution.taskSupport———✓remschema/2025-11-25/schema.ts:1270
Annotations.lastModified——✓✓✓schema/2025-06-18/schema.ts:1127
CompleteRequest…context.arguments——✓✓✓schema/2025-06-18/schema.ts:1333
Result.resultType (required)————✓schema/2026-07-28/schema.ts:234
InputRequiredResult————✓schema/2026-07-28/schema.ts:584
CacheableResult (ttlMs, cacheScope)————✓schema/2026-07-28/schema.ts:1081
_meta io.modelcontextprotocol/logLevel————depschema/2026-07-28/schema.ts:110
_meta io.modelcontextprotocol/related-task———✓remschema/2025-11-25/schema.ts:1332
URLElicitationRequiredError -32042———✓rem (reserved)schema/2025-11-25/schema.ts:181
MissingRequiredClientCapabilityError -32021————✓schema/2026-07-28/schema.ts:442

7. Error codes

CodeName / meaning2024-11-052025-03-262025-06-182025-11-252026-07-28Source
-32700Parse error✓✓✓✓✓ (ParseError)schema/2026-07-28/schema.ts:312
-32600Invalid Request✓✓✓✓ (+task-required misuse)✓2025-11-25/basic/utilities/tasks.mdx:775
-32601Method not found (also: server capability not advertised)✓✓✓✓ (+task-required tool not run as task)✓ + HTTP 4042026-07-28/basic/transports/streamable-http.mdx:271
-32602Invalid params: unknown tool/prompt, bad cursor, bad log level; legacy initialize version error example; 26-07 also missing _meta fields and resource-not-found✓✓✓✓✓ (expanded)2026-07-28/basic/index.mdx:381
-32603Internal error✓✓✓✓✓schema/2024-11-05/schema.ts:84
-32002Resource not found✓✓✓✓MUST NOT emit; clients SHOULD accept2026-07-28/basic/index.mdx:140
-32042URL elicitation required———✓MUST NOT emitschema/2025-11-25/schema.ts:181
-32020HeaderMismatch (headers ≠ body, or missing/malformed)————✓ (HTTP 400)schema/2026-07-28/schema.ts:434
-32021MissingRequiredClientCapability (data.requiredCapabilities)————✓ (HTTP 400)schema/2026-07-28/schema.ts:442
-32022UnsupportedProtocolVersion (data.supported, data.requested)————✓ (HTTP 400)schema/2026-07-28/schema.ts:450
-32000..-32019Legacy implementation-defined; receivers assume nothing (except -32002)(unreserved)(unreserved)(unreserved)(unreserved)policy2026-07-28/basic/index.mdx:117
-32020..-32099Reserved for the MCP spec————policy2026-07-28/basic/index.mdx:122
-32001 / -32003 / -32004Pre-release draft numbers for HeaderMismatch / MissingRequiredClientCapability / UnsupportedProtocolVersion; never in a release————renumbered2026-07-28/changelog.mdx:65
(HTTP) 400legacy: invalid/unsupported MCP-Protocol-Version, missing required session; 26-07: -32602 meta / -32020 / -32021 / -32022—✓ (session)✓✓✓2025-06-18/basic/transports.mdx:256
(HTTP) 403invalid Origin (explicit from 25-11); auth insufficient scope—✓ (auth)✓ (auth)✓✓2025-11-25/basic/transports.mdx:79
(HTTP) 404legacy: expired/unknown session ⇒ re-initialize; 26-07: unknown method + -32601—✓✓✓✓ (changed meaning)2025-03-26/basic/transports.mdx:201
(HTTP) 405GET/DELETE not offered—✓✓✓✓ (SHOULD for GET/DELETE)2026-07-28/basic/transports/streamable-http.mdx:683

8. HTTP headers

HeaderDir2024-11-052025-03-262025-06-182025-11-252026-07-28Source
Origin (validated)C→SMUST validateMUSTMUSTMUST; invalid ⇒ 403MUST; invalid ⇒ 4032026-07-28/basic/transports/streamable-http.mdx:58
Accept: application/json, text/event-streamC→S—MUST (POST); text/event-stream (GET)MUSTMUSTMUST (POST)2026-07-28/basic/transports/streamable-http.mdx:76
Content-Type (response application/json | text/event-stream)S→CSSE onlyMUST pick one✓✓✓2026-07-28/basic/transports/streamable-http.mdx:89
Mcp-Session-Idboth—MAY mint; MUST echosamesame (MCP-Session-Id)removed; ignore, don’t mint2025-03-26/basic/transports.mdx:187
MCP-Protocol-VersionC→S—SHOULD during OAuth metadata discovery onlyMUST after init; absent ⇒ assume 2025-03-26sameMUST every POST; = _meta else -320202026-07-28/basic/transports/streamable-http.mdx:252
Last-Event-IDC→S—resume via GETsamesame; always via GETremoved; ignore2025-11-25/basic/transports.mdx:179
Mcp-MethodC→S————REQUIRED all requests2026-07-28/basic/transports/streamable-http.mdx:290
Mcp-NameC→S————REQUIRED tools/call, resources/read, prompts/get2026-07-28/basic/transports/streamable-http.mdx:291
Mcp-Param-{Name} (from x-mcp-header)C→S————clients MUST mirror2026-07-28/basic/transports/streamable-http.mdx:374
X-Accel-Buffering: noS→C————SHOULD on SSE2026-07-28/basic/transports/streamable-http.mdx:136
Authorization: BearerC→S—MUST (when auth)MUSTMUSTMUST2026-07-28/basic/authorization/index.mdx:262
WWW-Authenticate (resource_metadata, scope, error="insufficient_scope")S→C—(401 only)MUST on 401header or well-known; scope SHOULDsame2025-11-25/basic/authorization.mdx:95
SSE id: / retry: fields (not headers)S→C—id MAYsamepriming event SHOULD; retry MUST be honouredremoved2025-11-25/basic/transports.mdx:115

Stateless and every revision: can mcpx do both?

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  whether one mcpx can serve and consume the stateless
              2026-07-28 protocol and every legacy revision at once, what
              "stateless" actually requires, where mcpx keeps state, and
              what stands in the way at 05c78b2.

The question: can mcpx speak the most modern, fully stateless protocol (2026-07-28) and also every older revision, in both directions?

The answer

The specification says yes, and says how. mcpx at 05c78b2 does not yet.

The specification allows exactly this. A dual-era server “MAY serve both eras concurrently on the same endpoint or process” (2026-07-28/basic/versioning.mdx:182), and it chooses per request: “A request carrying modern per-request _meta is served statelessly according to this revision. An initialize request selects legacy semantics, scoped to the stdio process (stdio) or the session (HTTP)” (2026-07-28/basic/versioning.mdx:176-180). A legacy session is therefore compatible with statelessness. What breaks statelessness is requiring state from a modern request, not having state somewhere.

“Stateless” is a property of the protocol surface, not of the process. A 2026-07-28 server may keep caches, durable tasks, open subscription streams and application handles. What it may not do is infer anything from the connection. It must not rely on earlier requests for version, capabilities or identity (2026-07-28/basic/index.mdx:191-193), should not require related operations to use the same connection (:196-197), must reference cross-request state “by an explicit identifier the client passes on each request” (:200-202), and “must not treat connection or process identity as a proxy for conversation or session continuity” (:204-208).

mcpx at 05c78b2 meets the dual-era shape. It serves initialize and per-request _meta on one endpoint, and it reads _meta before the connection’s handshake (internal/mcpserver/conn.go:133-146). It fails on details that decide interoperability:

  1. It cannot talk to a conformant 2026 peer in either direction. server/discover answers protocolVersions and the client reads protocolVersions; the schema field is supportedVersions (internal/mcpserver/server.go:534, internal/mcpclient/client.go:284, schema/2026-07-28/schema.ts:683). mcpx talks to itself because both halves are wrong the same way.
  2. Its modern input_required path needs the session 2026 removed. A requestState is bound to Mcp-Session-Id (internal/mcpserver/state.go:47-51, internal/mcpserver/server.go:1208-1230). A conformant sessionless 2026 client that declares elicitation gets -32603 "tools/call was still asking for input after 8 rounds" within milliseconds, and the upstream call is abandoned (internal/mcpserver/ask.go:195-209; reproduced on the wire, see the M1 transcript).
  3. It mints Mcp-Session-Id for modern clients on server/discover (internal/mcpserver/server.go:1214-1229). 2026-07-28 does not define the header, and tells a server that supports only that revision to “ignore it, and do not mint or echo session IDs” (2026-07-28/basic/transports/streamable-http.mdx:680-686). A dual-era server is not literally bound by that sentence. But a modern request is to be “served statelessly according to this revision”, and the harm is not the header, which a conformant client ignores. The harm is that mcpx then depends on the client echoing it (item 2).
  4. Its pool treats process identity as session continuity. A host that reaches mcpx over MCP is keyed as mcp-<pid> of the serving process (internal/cli/serve.go:292-297). Over the daemon’s /mcp that pid is the daemon’s, so every HTTP host shares one key. The comment says the host has “no way to tell us” (internal/cli/serve.go:289-290). opencode v2 does tell us: it sends _meta["ai.opencode/sessionID"] on every tools/call (v2:packages/core/src/mcp/client.ts:278), and mcpx discards it.

None of these is structural. Each has a known fix, and none needs mcpx to drop legacy support. §5 is the list.


1. What “stateless” requires of a 2026-07-28 server

The checklist, with mcpx’s status at 05c78b2. A server is stateless in the 2026-07-28 sense if all of these hold. “Dual-era” in the status column means the item applies to modern requests only, and mcpx handles legacy ones as the revision they negotiated.

#requirementsourcemcpx @ 05c78b2
1Every request carries _meta["io.modelcontextprotocol/protocolVersion"] and …/clientCapabilities; a request missing either is rejected with -32602 (HTTP 400)2026-07-28/basic/index.mdx:380-381partial: a request with a version and no capabilities is served as if it declared nothing (internal/mcpserver/conn.go:150-165). A request with neither is served as 2025-03-26, which is the dual-era reading (internal/mcpserver/conn.go:137-146)
2Version decided per request; an unsupported one gets -32022 with data.supported and data.requested2026-07-28/basic/versioning.mdx:48-67partial: -32022 is sent, but requested is "" for a modern request and the HTTP status is 200, where 2026 says 400 (internal/mcpserver/server.go:469-471; W13)
3Nothing inferred from earlier requests on the same connection2026-07-28/basic/index.mdx:191-193✓ for modern requests: _meta wins over the connection (internal/mcpserver/conn.go:133-136)
4A capability the request did not declare is never relied on; if one is required, -320212026-07-28/basic/index.mdx:387-392partial: send-conservatively holds, but -32021 is never used (rg -- -32021 internal finds nothing)
5Related operations need not share a connection or process2026-07-28/basic/index.mdx:196-197✗: a requestState retry must carry the same Mcp-Session-Id or it is refused “this requestState belongs to another session” (M4)
6Cross-request state is named by an explicit identifier the client sends2026-07-28/basic/index.mdx:200-202partial: task ids ✓; requestState ✓, but it also needs the session header (item 5)
7No protocol session: Mcp-Session-Id is neither minted nor required; GET has no stream (the literal SHOULD is for a server supporting only 2026-07-28)2026-07-28/basic/transports/streamable-http.mdx:680-686✗ minted on server/discover (item 3); ✓ GET answers 405 (internal/mcpserver/server.go:1124-1131)
8tools/list, prompts/list, resources/list do not vary per connection2026-07-28/server/tools.mdx:65-68✓: the MCP surface is built once per daemon (internal/cli/serve_ask.go:234-254)
9The server never sends a JSON-RPC request; it asks through input_required, and only on tools/call, resources/read and prompts/get2026-07-28/basic/patterns/mrtr.mdx:182-192✓ for modern peers (internal/mcpserver/ask.go:201-209, internal/mcpserver/ask.go:320-327)
10requestState is treated as attacker-controlled and integrity-protected. SHOULD bind the principal, a short expiry, and the method plus a digest of the parameters2026-07-28/basic/patterns/mrtr.mdx:232-243partial: HMAC-SHA256 with an expiry ✓. It binds the session, not a principal, and not the method or its arguments. A state was accepted on a different tool call on the same session (observed)
11Request-scoped notifications go only on that request’s stream, and log messages only if the request set logLevel2026-07-28/server/utilities/logging.mdx:63✓ by omission: mcpx forwards neither progress nor log messages to hosts
12The only long-lived construct is subscriptions/listen, whose state belongs to that request2026-07-28/basic/index.mdx:211-214✗ over HTTP: subscriptions/listen gets 202 with an empty body (W17). Partial on stdio: the acknowledgement has the wrong shape
13A closed response stream means cancellation; no resumption2026-07-28/basic/transports/streamable-http.mdx:233-237✓ by context propagation (inferred from code, not driven), and mcpx has no resumption
14server/discover answers supportedVersions, capabilities, optional instructions, ttlMs, cacheScopeschema/2026-07-28/schema.ts:678-697✗: protocolVersions, a top-level serverInfo, no ttlMs or cacheScope (W5)
15Every result has resultType; list, read and discover results have ttlMs and cacheScope; results SHOULD carry _meta["io.modelcontextprotocol/serverInfo"]2026-07-28/server/utilities/caching.mdx:13-16partial: resultType ✓ (wrong on the task handle); ttlMs, cacheScope and serverInfo ✗
16On HTTP, MCP-Protocol-Version, Mcp-Method and (for three methods) Mcp-Name are required and checked against the body; a mismatch is -32020 with HTTP 4002026-07-28/basic/transports/streamable-http.mdx:597-603✗: Mcp-Method and Mcp-Name are never read (W11). A version mismatch gets 400 with a body that is not a JSON-RPC error (W10)

What remains legitimately stateful in a conformant 2026 server: open listen streams, durable tasks (extension), client-held requestState, application handles, per-request auth context, process caches that no connection can observe, and legacy sessions for clients that opened with initialize.


2. Where mcpx keeps state

Every place mcpx remembers something across requests, verified at 05c78b2 (paths relative to pkgs/mcpx). The last two columns answer the question for 2026-07-28.

#itemwherekeyed bylifetimecompatible with 2026 statelessness?could it move into the request?
1pool instances (upstream processes and sessions)internal/pool/pool.go:57-72server × scope key (global, repo, worktree, cwd, session, parent-session, pid, call) × sharinguntil idle with no holders, a Min floor, Max with LRU eviction, restart or reloadyes: upstream-side and invisible to hosts. The key is the problem (row 13)the key can: from _meta["ai.opencode/sessionID"], or a server-minted handle passed as a tool argument, which is the 2026 pattern (2026-07-28/changelog.mdx:12)
2instance ↔ scope-key back-reference, for attributing questionsinternal/pool/request.go:18-33instanceinstanceyes (internal)n/a
3upstream schema cache (tools, resources, prompts, instructions)internal/pool/pool.go:121-128serveruntil list_changed, refresh or reload; persisted and reseededyesn/a. It could honour an upstream ttlMs
4upstream client state (era, negotiated version, capabilities)internal/mcpclient/client.go:69-100instanceinstanceyes: the spec tells clients to cache the era per server (2026-07-28/basic/versioning.mdx:148-152)n/a
5upstream HTTP session idinternal/mcpclient/http.go:29-30transporttransport; DELETE on closelegacy upstreams only; it should not be sent to a modern onen/a
6Streamable HTTP sessions (Mcp-Session-Id → connection)internal/mcpserver/server.go:1206-1230sess-<32 hex>idle proto.sessionIdle (30m), reaped only when another session is issued; DELETElegacy only. Minting one for a modern client (server/discover) is the violationfor legacy, no: a legacy client’s answer to elicitation/create arrives on a separate POST, and the session is how it finds the waiting request. For modern, its only job is binding requestState, which can bind to something in the request instead (row 9)
7the connection’s handshake state (capabilities, version)internal/mcpserver/conn.go:27-34connectionconnectionn/a for modern (_meta first)modern: already in the request; legacy: inherent
8pending server→client requests (legacy elicitation and sampling)internal/mcpserver/conn.go:52-53negative integer iduntil answered or timed outlegacy onlyn/a
9requestState signing key and tokensinternal/mcpserver/state.go:25-51key: per process, never persisted; token: {c: call id, b: session binding, e: expiry}key: process; token: proto.stateTTL (30m)partly: this is the 2026 mechanism; binding it to the session is what makes it depend on Mcp-Session-Idyes: bind the principal (or nothing; the HMAC already authenticates the token) plus a method and argument digest. The call id still names daemon-side state (row 10)
10interruptible calls: the upstream call keeps running as a daemon task while the client is away answeringinternal/daemon/routes_proto.go:53-65call id (= task id); (server, scope key) for attributionthe call; its result kept proto.askTTL (15m)yes, as a handle. The spec permits server-side state behind requestState and requires it for single-use (2026-07-28/basic/patterns/mrtr.mdx:238-243)no: a live upstream call cannot be serialised into a token
11ask-loop boundsinternal/mcpserver/timing.go:22-34requestproto.askTimeout 10m, proto.askRounds 8, proto.askPoll 500msn/a (per request)n/a
12elicitation broker rowsinternal/elicit/elicit.go:87-136question idelicit.ttl (120s) per question; rows persist in SQLiteyes (mcpx-internal, not protocol)n/a
13the MCP session key for pool scopeinternal/cli/serve.go:287-297$MCPX_SESSION_ID, else mcp-<pid> of the serving processprocessno: it is process identity used as session continuity, which 2026-07-28/basic/index.mdx:204-208 rules out; over /mcp every HTTP host shares the daemon’s pidyes: from request _meta or a tool argument
14MCP task storeinternal/mcpserver/tasks.go:31-38tsk-<16 hex>per-task TTL (client ttl or 10m)yes: server-minted handles are the extension’s model (but the shape is 2025-11-25’s, and tasks are not bound to their requestor)already a handle
15the daemon’s /v1 task store, a second storeinternal/daemon/ops.go:428-429tsk-…same TTL rulesyesshould be the same store as row 14
16subscription state (subs per URI, the listen filter)internal/mcpserver/conn.go:54-55connectionuntil replaced, DELETE or process exitlisten: 2026’s own mechanism; per-URI subs: legacylisten filter: already in the request
17recorded inbound cancellationsinternal/mcpserver/conn.go:56request idconnection; unbounded on stdion/anothing reads it
18pagination cursorinternal/mcpserver/server.go:1477-1513none: the cursor encodes the offsetnoneyes: already statelessalready in the request
19the MCP surface (lazyMCP)internal/cli/serve_ask.go:234-254daemondaemon (never rebuilt)yesn/a
20event-bus subscriptions for subscriptions/listeninternal/cli/serve.go:726-764listenlistenyesn/a
21the default stdio connectioninternal/mcpserver/server.go:148-151processprocesslegacy-inherent: stdio is one connection by naturen/a

Two observations the table makes:

  • Almost all of mcpx’s state is on the upstream side of the proxy. Pools, schema caches, upstream sessions and the broker are invisible to a host. 2026 says nothing about them and does not need to. A proxy that keeps a browser warm between calls keeps state, and statelessness never forbade that.
  • The state that faces the host is small and fixable. It is the HTTP session (legacy-only by right), the requestState binding, the MCP session key, and two task stores. Each has a request-scoped replacement that the specification already names.

The one thing that cannot become request-scoped is row 10, the live upstream call behind an input_required. Nor should it. mcpx is a single local process, not a fleet behind a load balancer, and the pattern’s motivation (“without requiring a shared storage layer across server instances”, 2026-07-28/basic/patterns/mrtr.mdx:30-31) does not apply to it. The requirement that does apply is that the handle is explicit, bound and expiring, and mcpx’s requestState already is.


3. Every revision, both directions

What happens today when each era meets mcpx. Server columns: a host of that revision connecting to mcpx. Client columns: mcpx connecting to an upstream server of that revision. The per-feature detail is in matrix-mcpx.md.

peer revisionmcpx as servermcpx as client
2024-11-05✗ initialize is refused with -32022 (W1, S1). Every legacy revision says a server MUST counter-offer a version it supports (2025-03-26/basic/lifecycle.mdx:132)partial, by accident. mcpx offers 2025-11-25; a 2024-11-05 server counter-offers 2024-11-05 and mcpx accepts whatever comes back (internal/mcpclient/client.go:261-275). There is no HTTP+SSE transport, so only stdio and a Streamable-HTTP-capable server work
2025-03-26✓ with gaps: JSON-RPC batches, which that revision says servers MUST receive, are rejected -32700 (2025-03-26/basic/index.mdx:97; W16); prompt title leaks to it✓
2025-06-18✓ with gaps: mcpx_call drops isError and flattens results to text for every revision (W20, W21)✓
2025-11-25✓ with gaps: sent capabilities.extensions, a 2026-only field✓ (notifications/tasks/status dropped)
2026-07-28partial: works for mcpx’s own client and for a client that keeps the Mcp-Session-Id from server/discover. A conformant client fails at server/discover (field names) and at input_required (session binding)✗ against a conformant modern-only server: discovery reads the wrong field; the legacy-first probe gives up when initialize is answered -32022 (internal/mcpclient/client.go:246); Mcp-Method/Mcp-Name are never sent, so a conformant HTTP server answers 400

“Every older one” also needs a floor. The oldest published revision is 2024-11-05, and mcpx’s is 2025-03-26 (internal/mcpserver/revisions.go:14). Serving 2024-11-05 is cheap to add, and it is the only revision mcpx refuses outright. It means a counter-offer in negotiate, plus one floors row for each thing 2025-03-26 added (audio, tool annotations, the completions capability: 2025-03-26/changelog.mdx:18, :25-27). With those rows the downgrade() audio branch, dead today, starts to matter.


4. The conflict at the centre

Statelessness and “accept liberally” pull in opposite directions in one place: a request that carries neither _meta nor a session. 2026-07-28 says such a request is malformed (2026-07-28/basic/index.mdx:380-381). The legacy HTTP rules say a server that receives no MCP-Protocol-Version “SHOULD assume protocol version 2025-03-26” (2025-06-18/basic/transports.mdx:250-254), and a legacy client may legitimately never send a session header when the server issued none. mcpx takes the legacy reading and serves the request as 2025-03-26 (internal/mcpserver/conn.go:137-146).

For a dual-era server that is the right call. Rejecting it would break every legacy client that ignores sessions, and nothing in 2026-07-28 tells a dual-era server to prefer the modern reading. The specification never states it, and this page records it so nobody “fixes” it into a regression. See conflicts.md.


5. What it would take

Ordered by how much interoperability each item buys. The register row for each is in the linked area file. The PROTOCOL work in progress at the time of writing (a modern-first probe and conformance fixes) is expected to close some of these; the status column in each register file is where that shows.

#changeareaissueeffort
1supportedVersions on both sides; serverInfo into result _meta; ttlMs and cacheScope on discover, list and read resultslifecycle, pagination-caching#199, #200S
2Bind requestState to the request, not the session: the method, an argument digest, the principal if there is one, and the expiry. Stop minting Mcp-Session-Id for modern clients. Fix the no-binding busy loop so a question the client cannot receive goes back to the broker, as the comment says it doesmrtr, transports#201M
3Client: treat -32022 from initialize as a modern server and retry with server/discover; send Mcp-Method, Mcp-Name and Mcp-Param-*lifecycle, transports#200S–M
4Server: check Mcp-Method and Mcp-Name; answer header mismatches with -32020 and HTTP 400; map -32601 → 404 and -32022/-32021/-32602 → 400 for modern requeststransports, errors#199S
5Take the pool’s session key from _meta["ai.opencode/sessionID"] (or a namespaced equivalent) when present, instead of mcp-<pid>process-model, meta#211S
6Validate Origin on /mcp and /v1. It has been a MUST since 2024-11-05, and it is the one gap here with a working attack: DNS rebinding from a web page to a loopback port that runs scriptstransports#204S
7subscriptions/listen over HTTP as an SSE response stream; the acknowledgement as notifications/subscriptions/acknowledged with _meta subscription idsnotifications#208M
8The tasks extension for 2026 clients (tasks/update, resultType: "task", ttlMs, no tasks/list/tasks/result), and no extensions or core tasks capability to a revision that does not define ittasks, capabilities#209M–L
9Counter-offer on initialize (2024-11-05 and unknown versions) and add the 2024-11-05 floorslifecycle#202S

Nothing on this list conflicts with keeping every legacy revision. Items 1–8 touch only the modern path or add checks the legacy path already skips.


Evidence

The wire observations (W*, M*, S*) come from a session recorded against a binary built from 05c78b2 with an isolated configuration, state and socket, driving the repository’s own test servers (internal/testsupport/fakemcp, internal/testsupport/askmcp). W1, W5, W12, W15, W16, W17, W18, W20, W21, M1 and M2 were reproduced independently for this page. The requests, verbatim:

  • W5 server/discover with 2026 _meta → HTTP 200, a Mcp-Session-Id header, and {"capabilities":{…,"extensions":{"io.modelcontextprotocol/tasks":{}},…,"tasks":{…}},"instructions":…,"protocolVersions":[…],"serverInfo":{…}}.
  • M1 tools/call mcpx_call → ask.need_repo with 2026 _meta declaring elicitation, no session → {"error":{"code":-32603,"message":"tools/call was still asking for input after 8 rounds"}} in about 40 ms.
  • M2 the same after taking Mcp-Session-Id from server/discover → {"resultType":"input_required","inputRequests":{"elc-…":{"method":"elicitation/create",…}},"requestState":"<base64>.<hmac>"}. The base64 part decodes to {"c":"tsk-…","b":"sess-…","e":<unix expiry>}.
  • M4 the M3 retry without the session header → {"error":{"code":-32602,"message":"this requestState belongs to another session"}}.
  • W17 subscriptions/listen over HTTP → 202 Accepted, Content-Length: 0.
  • W13 a request whose _meta version is 9999-01-01 → HTTP 200, -32022, "requested":"".

What each says a server should be

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare, area:protocol
description:  the character of an MCP server as each revision and each
              product imagines it, in a paragraph each, and what mcpx has
              to be to satisfy all of them at once.

This page is the philosophy, one short account per revision and per product. The exhaustive list of what a server MUST and SHOULD do, revision by revision, belongs to the protocol work (docs/spec/server-obligations.md, which that work is writing; it is not on main at 05c78b2). The feature-by-feature table is matrix-revisions.md.

The revisions

2024-11-05: a stateful peer over a session. “MCP provides a stateful session protocol” (2024-11-05/architecture/index.mdx:8). One client and one server open a session with initialize, and version and capabilities are fixed for its life. Either side may send the other a request at any time: the server samples and lists roots, both ping. Transports are stdio or a two-endpoint HTTP+SSE with a persistent event stream. There is no authorization in the core. The server is a long-lived companion process that remembers its client.

2025-03-26: the same session, made deployable on the web. Streamable HTTP puts everything on one endpoint, adds optional Mcp-Session-Id sessions and resumable SSE, and treats a dropped connection as not a cancellation (2025-03-26/basic/transports.mdx:120-122). OAuth 2.1 arrives with the server as its own authorization server, along with batching (for one revision), audio, tool annotations, and a completions capability (2025-03-26/changelog.mdx:9-27). The server is still a session. It is now also something you can put behind a URL.

2025-06-18: a session that is an OAuth resource server. Batching goes. Every HTTP request after initialization carries MCP-Protocol-Version. The server discovers its authorization server through RFC 9728 and validates token audiences (RFC 8707). Respecting negotiated capabilities becomes a MUST (2025-06-18/changelog.mdx:12-26). Structured tool output, resource links, title, formal _meta rules and elicitation all arrive. The server can now ask the user something, and it describes its output with a schema.

2025-11-25: the most elaborate stateful server. Sessions, resumable and now pollable SSE (the server may drop a stream and let the client reconnect), experimental durable tasks with their own cancel and progress rules, URL-mode elicitation with its own error code, OIDC discovery, Client ID Metadata Documents, step-up scopes and icons (2025-11-25/changelog.mdx:9-40). The server is a long-running service with jobs, still one session per connection.

2026-07-28: a stateless request/response service. “MCP is a stateless protocol: every request is self-contained and carries its own protocol version and capabilities” (2026-07-28/architecture/index.mdx:8-9). No handshake, no session, no GET stream, no resumption, no ping, no server-initiated request. The server asks by returning input_required and waiting for the client to retry. Lists must not vary per connection. Results say how long they may be cached, and a single opted-in subscriptions/listen stream is the only long-lived construct (2026-07-28/changelog.mdx:12-34). Roots, sampling and logging are deprecated. The server is a function of the request, and anything that spans requests is a handle the client carries. A server may still serve legacy clients alongside, as a dual-era server (2026-07-28/basic/versioning.mdx:128-130).

The arc across the five: from a companion process that remembers its client to a service that remembers nothing it was not handed. mcpx has to sit at both ends of that arc at once.

The products

opencode v1 (1.18.31) consumes a tools-first, text-first server over one long-lived connection per server, per directory, per opencode process. It connects every configured server when MCP is first touched and waits for all of them (v1:packages/opencode/src/mcp/index.ts:505). It never restarts one that dies (:448), and it gives every session in that directory the same client with nothing identifying the caller (v1:packages/opencode/src/mcp/catalog.ts:54). It declares only roots (v1:packages/opencode/src/mcp/index.ts:46), so a server must not need elicitation, sampling or tasks, and it speaks 2025-11-25 at most. Text, images and embedded resources reach the model; audio and resource_link do not (v1:packages/opencode/src/session/tools.ts:429). Server instructions and a declared resources capability cost context on every request (v1:packages/opencode/src/session/system.ts:129, v1:packages/opencode/src/session/tools.ts:136). With OPENCODE_EXPERIMENTAL set, as it is on this machine, the tools move behind code mode’s execute (v1:packages/opencode/src/session/tools.ts:388).

opencode v2 (2.0.3) consumes servers as libraries for its own code mode. It holds one connection per server per Location, lives as long as the background daemon, and shares that connection with every session in every attached window (v2:packages/core/src/mcp/index.ts:152, v2:packages/core/src/location-services.ts:52). It expects typed tools with names matching ^[A-Za-z0-9_-]{1,64}$ (v2:packages/core/src/tool.ts:297). It prefers results as structuredContent or JSON text, which it turns into program values (v2:packages/core/src/tool/mcp.ts:96). It answers form and URL elicitations, attributed to the Location rather than the session (v2:packages/core/src/mcp/index.ts:80). It does not ask the server for isolation; it tells the server which session is calling, in _meta["ai.opencode/sessionID"], and leaves partitioning to the server (v2:packages/core/src/mcp/client.ts:278). It speaks 2025-11-25 by default and 2026-07-28 when configured (v2:packages/schema/src/mcp.ts:23), allows 12-hour calls (v2:packages/core/src/mcp/client.ts:34), and expects a server that is itself code mode to be configured codemode: false (v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:16).

lootbox treats a server as a long-lived process it connects to once, at daemon start, and shares among all callers, pinging it to decide whether it is alive (.lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:221-222). It declares capabilities: {}, so a server that elicits, samples or asks for roots is refused (.lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153). It assumes one page of tools and one page of resources, and names that reduce harmlessly to [A-Za-z0-9_] (.lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:69). It assumes a server is stateless enough to share. Its one concession to stateful servers is rewriting a port it finds in their command line (.lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:82-133). Results reach the script as raw envelopes.

Cloudflare code mode starts from “Almost every MCP server is just a wrapper around an existing traditional API” (https://blog.cloudflare.com/code-mode/, “But MCP is still useful, because it is uniform”). A server is a remote, OAuth-authorised HTTP endpoint that an Agent connects to once. The Agents MCP client offers auto, streamable-http and sse, and no stdio (https://developers.cloudflare.com/agents/model-context-protocol/apis/client-api/, “Transport options”). The server enforces its own authorization. It describes its tools well enough to generate typed methods, with names unique after sanitisation or the connector throws. A large API should itself be a code-mode server with two tools, search and execute, at about 1,000 tokens (https://blog.cloudflare.com/code-mode-mcp/, “Server-side Code Mode”). Side-effecting tools should be safe to pause, replay in order and, ideally, revert (https://developers.cloudflare.com/agents/tools/codemode/how-it-works/, “Deterministic replay”, “Rollback”).

mcpx (as a server) describes itself as a server that “runs MCP servers and exposes them through a few tools rather than many” (internal/mcpserver/server.go:397): a stable, small surface (mcpx_call, mcpx_exec, discovery tools) whose real work happens next to pooled upstream processes. Its rules for itself are in ../protocol.md: speak every revision, accept liberally, send conservatively, carry questions across the era boundary.

What mcpx has to be to satisfy all of them

Reading the accounts side by side gives mcpx’s job:

  • To 2026-07-28 and to opencode v2, a function of the request. Version, capabilities, and now identity: v2 hands over the session in _meta, and 2026 forbids inferring it from the connection. Anything spanning requests is a handle.
  • To the legacy revisions and to opencode v1, a patient session. It holds the initialize agreement, sends questions on the wire, and never replies to a notification.
  • To lootbox-style and Cloudflare-style callers, a catalogue. Typed names that sanitise safely and do not collide, with results a program can use (structured when possible).
  • To every upstream, the host they cannot see. It answers ping, roots, elicitation and sampling on the host’s behalf, or carries them to the host when the host can answer.

The places where mcpx does not yet meet one of these are the register’s mcpx missing rows (README).

Authorization

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Every authorization mechanism per MCP revision, what mcpx, opencode, lootbox and Cloudflare do with it.

2024-11-05 has no authorization; 2025-03-26 adds an optional OAuth 2.1 profile for HTTP, and every revision since reshapes it (resource server and RFC 9728/8707 in 2025-06-18; OIDC discovery, Client ID Metadata Documents and step-up scopes in 2025-11-25; RFC 9207 iss, application_type, issuer-keyed credentials and deprecated DCR in 2026-07-28). For mcpx two facts dominate: as a client it performs none of the OAuth flow and does not even apply its own static auth: block, so the only credential that reaches an upstream is a raw headers: map or an environment variable; as a server it authenticates nothing, on /mcp or on the /v1 endpoint that runs scripts.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
AUTH-01No authorization in the core protocol2025-03-26 has24-11 ✓ · 25-03 — · 25-06 — · 25-11 — · 26-07 —partial — headers:/env: work; auth: block inert+ lowSlow
AUTH-02OAuth 2.1 profile for HTTP; stdio takes environment credentials2025-03-26 has impl deferred opencode v1 has opencode v2 has24-11 — · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 ✓ · v2 ✓✗ — config shape only; flow deferred+ highLhigh
AUTH-03Access token as Authorization: Bearer, never in the query2025-03-26 has24-11 — · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — only via raw headers:; auth.bearer inert+ medSmed
AUTH-04PKCE required; refuse AS without code_challenge_methods_supported2025-03-26 has 2025-11-25 has impl deferred opencode v1 has opencode v2 has25-03 ✓ PKCE · 25-06 ✓ · 25-11 ✓ + check · 26-07 ✓✗ — deferred with the flow+ medSlow
AUTH-05AS metadata at MCP base URL; default-endpoint fallback2025-03-26 has 2025-06-18 removes has better replacement impl deferred25-03 ✓ · 25-06 — · 25-11 — · 26-07 —✗ — deferred+ lowSlow
AUTH-06MCP server is a resource server; RFC 9728 metadata2025-06-18 has impl deferred25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — client discovery deferred; server has no auth+ highMmed
AUTH-07Finding PRM: WWW-Authenticate header, then well-known URIs2025-06-18 has 2025-11-25 has impl deferred25-06 header MUST · 25-11 header or well-known · 26-07 same✗ — deferred+ highSlow
AUTH-08RFC 8707 resource parameter on authorization and token requests2025-06-18 has impl deferred opencode v2 has25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v2 ✓✗ — OAuth.Resource field exists, unused+ medSmed
AUTH-09Token audience validation; no token passthrough2025-06-18 has25-06 ✓ · 25-11 ✓ · 26-07 ✓n/a — /mcp accepts no tokens to forward+ medShigh
AUTH-10OIDC Discovery and ordered well-known probing2025-11-25 has impl deferred25-11 ✓ · 26-07 ✓✗ — deferred+ medSlow
AUTH-11Dynamic Client Registration: SHOULD, then MAY, then deprecated2025-03-26 has 2026-07-28 deprecates has better replacement impl deferred opencode v1 has25-03 SHOULD · 25-06 SHOULD · 25-11 MAY · 26-07 dep · opencode v1 ✓✗ — no DCR; config prefers CIMD+ lowMlow
AUTH-12application_type required in DCR2026-07-28 has impl deferred26-07 ✓✗ — deferred+ lowSlow
AUTH-13Client ID Metadata Documents as client_id2025-11-25 has 2026-07-28 has impl deferred opencode v2 has25-11 ✓ · 26-07 SHOULD · opencode v2 ✓✗ — clientId comment prefers it; unused+ highMlow
AUTH-14Scope selection and step-up on insufficient_scope2025-11-25 has impl deferred25-11 ✓ · 26-07 ✓ (+ union SHOULDs)✗ — scopes field exists, unused+ medMlow
AUTH-15RFC 9207 iss check on the authorization response2026-07-28 has impl deferred26-07 ✓✗ — deferred+ medSlow
AUTH-16Fetched metadata issuer must equal the issuer used2026-07-28 has impl deferred26-07 ✓✗ — deferred+ medSlow
AUTH-17Credentials keyed by issuer; re-register on AS change2026-07-28 has impl deferred opencode v1 has opencode v2 has26-07 ✓ · opencode v1 by name · v2 by name+url✗ — no token store+ medSmed
AUTH-18Refresh-token handling rules2026-07-28 has impl deferred opencode v1 has26-07 ✓ · opencode v1 lists refresh_token grant✗ — deferred+ medSlow
AUTH-19oauth-client-credentials extension (machine-to-machine)2026-07-28 has26-07 extension✗ — not declared or performed+ medMlow
AUTH-20enterprise-managed-authorization extension (IdP, ID-JAG)2026-07-28 has does not match mcpx's goal26-07 extensionn/a — out of scope for a local tool− scopeLlow
AUTH-21OAuth redirect listener: fixed port vs ephemeralopencode v1 has opencode v2 has impl deferredopencode v1 127.0.0.1:19876 · v2 configured or ephemeral✗ — redirect field only+ medSlow
AUTH-22Per-server auth: block parsed and never appliedmcpx missingmcpx only✗ broken — Auth.Resolve has no caller (#205)+ highShigh
AUTH-23/mcp and /v1 authenticate nobody2024-11-05 has mcpx missing24-11 SHOULD · 25-03 SHOULD · 25-06 SHOULD · 25-11 SHOULD · 26-07 SHOULD✗ — loopback plus socket permissions only (#204)+ highMhigh
AUTH-24Code-execution endpoints accept cross-origin simple requestsmcpx missingmcpx /v1/exec · lootbox /ws✗ — no Origin or Content-Type check (#204)+ highShigh
AUTH-25Scripts see credentials: bindings vs inherited environmentdoes not match mcpx's goalCloudflare bindings · opencode none · lootbox unreadable · mcpx full envn/a — scripts inherit full os.Environ() by design− designSmed

AUTH-01 No authorization in the core protocol

  • What. 2024-11-05 says authentication and authorization are not part of the core specification; the parties may negotiate their own.
  • Where. 2024-11-05 only. Every later revision has the OAuth profile (AUTH-02).
  • mcpx @ 05c78b2. mcpx does not serve 2024-11-05, but as a client its only working credentials are exactly this “negotiate your own” route: a static headers: map for HTTP (internal/pool/pool.go:379) and an env: map plus the daemon’s inherited environment for stdio (internal/pool/pool.go:372). The richer auth: declaration (bearer/basic/header/query/env/oauth, internal/mcpauth/mcpauth.go:29-57) is never applied (AUTH-22).
  • Value to mcpx. + low: static credentials are what most servers need.
  • Effort. S — nothing new; fix AUTH-22.
  • Risk. None if not done.
  • Detail. “Custom” authentication survives into 2026-07-28: clients and servers MAY still negotiate their own (2026-07-28/basic/index.mdx:227), so a static header is conformant in every revision, not a legacy crutch.
  • Sources. 2024-11-05/basic/index.mdx:75 “Authentication and authorization are not currently part of the core MCP specification,”; internal/mcpauth/mcpauth.go:29-57; internal/pool/pool.go:372 “Env: p.cfg.Env,”

AUTH-02 OAuth 2.1 profile for HTTP; stdio takes environment credentials

  • What. Authorization is optional. When implemented, HTTP transports SHOULD follow the OAuth 2.1 profile; stdio transports SHOULD NOT and take credentials from the environment instead.
  • Where. 2025-03-26 through 2026-07-28, framing unchanged (https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). opencode v1 (PKCE + DCR) and v2 (adds CIMD, RFC 8707, ephemeral callback) both perform the client flow.
  • mcpx @ 05c78b2. The OAuth configuration shape exists (clientId, scopes, resource, redirect, internal/mcpauth/mcpauth.go:59-77); the flow does not. The type comment defers it knowingly (internal/mcpauth/mcpauth.go:62-64) and docs/protocol.md §6 lists it as still missing (docs/protocol.md:378). The stdio half is served: stdio children get env: with InheritEnv: true (internal/pool/pool.go:369-375).
  • Value to mcpx. + high: GitHub, Linear and most hosted servers are unreachable without it, except through a wrapper such as mcp-remote.
  • Effort. L — discovery, PKCE, a browser step, a callback listener, a token store, refresh.
  • Risk. Not done: OAuth-protected servers cannot be pooled. Done: a daemon that holds tokens for many servers becomes a credential store and must key them correctly (AUTH-17).
  • Detail. The comment claims a server requiring OAuth “reports that clearly rather than failing at the first request with a 401”; nothing calls the code that would report it (AUTH-22), so today it is exactly a bare 401. url-mode elicitation is the natural carrier for the browser step (a daemon has no browser of its own). The same HTTP/stdio split is restated in 2026-07-28 base protocol (2026-07-28/basic/index.mdx:225).
  • Sources. 2025-03-26/basic/authorization.mdx:15 “Authorization is OPTIONAL for MCP implementations. When supported:”; 2025-03-26/basic/authorization.mdx:18 “- Implementations using an STDIO transport SHOULD NOT follow this specification, and”; internal/mcpauth/mcpauth.go:63 “and is not yet implemented; a server requiring it reports that clearly”; docs/protocol.md:378 “OAuth for remote servers. Declared and not performed, unchanged.”; v1:packages/opencode/src/mcp/oauth-provider.ts:11; v2:packages/core/src/mcp/oauth.ts:25

AUTH-03 Access token as Authorization: Bearer, never in the query

  • What. The client MUST send the access token in the Authorization: Bearer header on every HTTP request, and MUST NOT put it in the URI query string.
  • Where. 2025-03-26 through 2026-07-28.
  • mcpx @ 05c78b2. A token reaches an upstream only if the user writes the header by hand in headers:, which the pool passes to the HTTP transport (internal/pool/pool.go:379). auth: {type: bearer} would build it, but is inert (AUTH-22).
  • Value to mcpx. + med: this is the step every protected server needs, OAuth or not.
  • Effort. S — call Auth.Resolve and merge its headers.
  • Risk. Not done: users who follow the documented auth.bearer shape send no token.
  • Detail. 2025-03-26 adds “even if they are part of the same logical session” to the every-request rule; 2026-07-28 drops the clause because sessions are gone. mcpx’s auth type query exists to put a static credential in the URL (Resolved.Query); that is legitimate for API keys but would violate the query-string MUST if it ever carried an OAuth access token.
  • Sources. 2025-03-26/basic/authorization.mdx:270 “Authorization: Bearer ”; 2025-03-26/basic/authorization.mdx:276 “2. Access tokens MUST NOT be included in the URI query string”; 2026-07-28/basic/authorization/index.mdx:262 “1. MCP client MUST use the Authorization request header field defined in”; internal/pool/pool.go:379 “Headers: p.cfg.Headers,”

AUTH-04 PKCE required; refuse AS without code_challenge_methods_supported

  • What. PKCE is required for every client from 2025-03-26. From 2025-11-25 the client MUST also check code_challenge_methods_supported in the AS or OIDC metadata and refuse to proceed if it is absent.
  • Where. PKCE 2025-03-26 onward; the metadata check 2025-11-25 and 2026-07-28. opencode v1 and v2 use PKCE (v1 “PKCE + DCR”; v2 adds CIMD on top).
  • mcpx @ 05c78b2. Not implemented; part of the deferred flow (internal/mcpauth/mcpauth.go:62-64).
  • Value to mcpx. + med: a guard of a few lines once the flow exists.
  • Effort. S.
  • Risk. None today.
  • Detail. OIDC providers MUST include the field from 2025-11-25; an AS whose metadata omits it “does not support PKCE” by definition, so the refusal is not optional.
  • Sources. 2025-03-26/basic/authorization.mdx:318 “2. PKCE is REQUIRED for all clients”; 2025-11-25/basic/authorization.mdx:605 “If code_challenge_methods_supported is absent, the authorization server does not support PKCE and MCP clients MUST refuse to proceed.”; v1:packages/opencode/src/mcp/oauth-provider.ts:11

AUTH-05 AS metadata at MCP base URL; default-endpoint fallback

  • What. In 2025-03-26 the MCP server is also the authorization server: the client derives the authorization base URL by discarding the path of the MCP URL, reads RFC 8414 metadata there, and if none exists MUST fall back to /authorize, /token and /register.
  • Where. 2025-03-26 only. 2025-06-18 separates the roles and replaces this with protected-resource metadata (AUTH-06).
  • mcpx @ 05c78b2. Not implemented (deferred).
  • Value to mcpx. + low: only 2025-03-26-era servers that never published metadata need it.
  • Effort. S once the flow exists.
  • Risk. None.
  • Detail. 2025-03-26 also defines MCP-Protocol-Version here, as a SHOULD during metadata discovery, although 2026-07-28 says revisions before 2025-06-18 did not define the header (the transports area records the timeline).
  • Sources. 2025-03-26/basic/authorization.mdx:143 “The authorization base URL MUST be determined from the MCP server URL by discarding”; 2025-03-26/basic/authorization.mdx:158 “MUST use the following default endpoint paths relative to the [authorization base”; 2025-03-26/basic/authorization.mdx:135-139

AUTH-06 MCP server is a resource server; RFC 9728 metadata

  • What. From 2025-06-18 an MCP server is an OAuth resource server. It MUST publish RFC 9728 Protected Resource Metadata naming its authorization_servers, and clients MUST use that document to find the AS.
  • Where. 2025-06-18 through 2026-07-28.
  • mcpx @ 05c78b2. As a client: deferred. As a server: /mcp has no authorization at all (AUTH-23), so publishing no metadata is consistent.
  • Value to mcpx. + high: required to reach any modern protected server.
  • Effort. M.
  • Risk. None now.
  • Detail. The role split is why AUTH-05’s path-stripping disappears: the AS may live on another host entirely.
  • Sources. 2025-06-18/basic/authorization.mdx:61 “1. MCP servers MUST implement OAuth 2.0 Protected Resource Metadata”; 2025-06-18/changelog.mdx:16 “Classify MCP servers as [OAuth Resource Servers]”

AUTH-07 Finding PRM: WWW-Authenticate header, then well-known URIs

  • What. 2025-06-18: servers MUST send WWW-Authenticate with resource_metadata on a 401. 2025-11-25 (SEP-985) relaxes the server side to “header or /.well-known/oauth-protected-resource[/<path>]”, and makes clients support both: header first, then the path-inserted well-known URI, then the root one.
  • Where. 2025-06-18 (header only), 2025-11-25 and 2026-07-28 (either).
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + high: without the fallback order, servers that publish only the well-known document look unprotected until the first 401.
  • Effort. S within the flow.
  • Risk. None now.
  • Detail. The same header later carries scope and error="insufficient_scope" (AUTH-14).
  • Sources. 2025-06-18/basic/authorization.mdx:87 “MCP servers MUST use the HTTP header WWW-Authenticate when returning a 401 Unauthorized”; 2025-11-25/basic/authorization.mdx:101 “MCP clients MUST support both discovery mechanisms and use the resource metadata URL”

AUTH-08 RFC 8707 resource parameter on authorization and token requests

  • What. Clients MUST send resource=<canonical MCP server URI> in both the authorization and the token request, whether or not the AS understands it.
  • Where. 2025-06-18 through 2026-07-28. opencode v2 checks that the resource named in protected-resource metadata covers the configured server URL, and falls back to the configured URL when the server publishes none.
  • mcpx @ 05c78b2. The config field exists and its comment states the rule (internal/mcpauth/mcpauth.go:72-74); nothing sends it.
  • Value to mcpx. + med: mandatory once OAuth exists.
  • Effort. S within the flow.
  • Risk. Not done: tokens without audience binding.
  • Detail. Canonical URI: lowercase scheme and host, no trailing slash unless significant. opencode v2’s comment explains the fallback: the SDK sends no resource when the server publishes no metadata, and some ASes require one. v2 also keeps the configured URL (without the ?codemode=false it appends, see the plugin-APIs area) as the OAuth identity.
  • Sources. 2025-06-18/basic/authorization.mdx:194 “MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in [RFC 8707]”; internal/mcpauth/mcpauth.go:72-74; v2:packages/core/src/mcp/oauth.ts:141-146 “if (!checkResourceAllowed({ requestedResource: identity, configuredResource: resource }))”

AUTH-09 Token audience validation; no token passthrough

  • What. A server MUST validate that a token was issued for it and MUST NOT accept or relay any other token; it must not pass the client’s token through to an upstream API.
  • Where. 2025-06-18 through 2026-07-28.
  • mcpx @ 05c78b2. n/a today: /mcp accepts no token (AUTH-23), so there is nothing to forward.
  • Value to mcpx. + med: it fixes a rule for later. mcpx is a proxy; if /mcp ever takes bearer tokens, the agent’s token must never become the upstream’s Authorization.
  • Effort. S.
  • Risk. High if violated: token passthrough is the confused-deputy case the rule exists for.
  • Detail. mcpx’s upstream credentials come from its own config (headers:, future OAuth), which is already the shape the rule requires.
  • Sources. 2025-06-18/basic/authorization.mdx:275 “MCP servers MUST NOT accept or transit any other tokens.”

AUTH-10 OIDC Discovery and ordered well-known probing

  • What. From 2025-11-25 an AS MUST offer RFC 8414 or OpenID Connect Discovery, and clients MUST support both. For an issuer with a path they try oauth-authorization-server/<path>, openid-configuration/<path>, then <path>/.well-known/openid-configuration; without a path, the two root forms.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + med: Google- and Entra-style issuers publish only OIDC documents.
  • Effort. S.
  • Risk. None now.
  • Detail. 2026-07-28 adds an identity check on the fetched document (AUTH-16).
  • Sources. 2025-11-25/basic/authorization.mdx:66 “5. MCP authorization servers MUST provide at least one of the following discovery mechanisms:”; 2025-11-25/basic/authorization.mdx:135 “For issuer URLs with path components (e.g., https://auth.example.com/tenant1), clients MUST try endpoints in the following priority order:”

AUTH-11 Dynamic Client Registration: SHOULD, then MAY, then deprecated

  • What. RFC 7591 registration weakens each revision: SHOULD (2025-03-26, 2025-06-18), MAY (2025-11-25), deprecated in 2026-07-28 in favour of Client ID Metadata Documents, with removal no earlier than the first revision on or after 2027-07-28.
  • Where. 2025-03-26 through 2026-07-28. opencode v1 registers dynamically with client_name: "OpenCode".
  • mcpx @ 05c78b2. No DCR. The clientId comment already prefers a CIMD URL (internal/mcpauth/mcpauth.go:66-68).
  • Value to mcpx. + low: some older ASes only do DCR; everything newer takes CIMD or a pre-registered id.
  • Effort. M.
  • Risk. None now.
  • Detail. Registration priority in 2025-11-25 and 2026-07-28: pre-registered, then CIMD if the AS advertises client_id_metadata_document_supported, then DCR if it has a registration_endpoint. If mcpx ever does DCR it must also send application_type (AUTH-12).
  • Sources. 2025-03-26/basic/authorization.mdx:42 “2. MCP auth implementations SHOULD support the OAuth 2.0 Dynamic Client Registration”; 2025-11-25/basic/authorization.mdx:60 “3. Authorization servers and MCP clients MAY support the OAuth 2.0 Dynamic Client Registration”; 2026-07-28/basic/authorization/client-registration.mdx:141 “Dynamic Client Registration is deprecated. New implementations should use”; 2026-07-28/deprecated.mdx:29 “| [Dynamic Client Registration]”; v1:packages/opencode/src/mcp/oauth-provider.ts:46 “client_name: "OpenCode",”

AUTH-12 application_type required in DCR

  • What. A client registering dynamically MUST send application_type: "native" for desktop, CLI and localhost redirects, "web" for remote browser apps.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + low: mcpx is "native"; DCR itself is deprecated.
  • Effort. S.
  • Risk. Not done: OIDC providers default to "web" and reject a localhost redirect URI.
  • Detail. Clients MUST be prepared for registration failure and MAY retry with an adjusted type.
  • Sources. 2026-07-28/basic/authorization/client-registration.mdx:160 “MCP clients MUST specify an appropriate application_type”

AUTH-13 Client ID Metadata Documents as client_id

  • What. The client’s client_id is an HTTPS URL to a JSON document (client_id, client_name, redirect_uris at least) that the AS fetches; the AS advertises client_id_metadata_document_supported: true.
  • Where. 2025-11-25 (one registration approach), 2026-07-28 (AS and clients SHOULD support). opencode v2 offers https://opencode.ai/oauth/opencode/client.json.
  • mcpx @ 05c78b2. The config comment prefers a CIMD URL (internal/mcpauth/mcpauth.go:66-68); nothing uses it.
  • Value to mcpx. + high: a daemon with no registration step, portable across ASes (no re-registration when the AS changes, AUTH-17). − someone has to host the document over HTTPS.
  • Effort. M.
  • Risk. None.
  • Detail. 2026-07-28 security considerations: a CIMD cannot by itself stop localhost impersonation, so ASes SHOULD warn on localhost-only redirect URIs and MUST show the redirect hostname. A local daemon’s redirect is exactly that case, so users will see the warning.
  • Sources. 2026-07-28/basic/authorization/security-considerations.mdx:95 “Client ID Metadata Documents cannot prevent localhost URL impersonation by themselves.”; 2025-11-25/basic/authorization.mdx:207 “2. Use Client ID Metadata Documents if the Authorization Server indicates if the server supports it”; 2026-07-28/basic/authorization/index.mdx:65 “2. Authorization servers and MCP clients SHOULD support [OAuth Client ID Metadata Documents]”; v2:packages/core/src/mcp/oauth.ts:25 “export const CLIENT_METADATA_URL = "https://opencode.ai/oauth/opencode/client.json"”

AUTH-14 Scope selection and step-up on insufficient_scope

  • What. Servers SHOULD put scope in the 401 WWW-Authenticate; clients prefer that, else all scopes_supported. At runtime a 403 with error="insufficient_scope" triggers step-up re-authorization.
  • Where. 2025-11-25 and 2026-07-28. 2026-07-28 adds two SHOULDs: a challenge names every scope the operation needs, and the client requests the union with scopes already granted.
  • mcpx @ 05c78b2. The scopes comment already says the challenge is authoritative (internal/mcpauth/mcpauth.go:69-71); nothing reads a challenge.
  • Value to mcpx. + med: real remote servers (GitHub) scope per tool.
  • Effort. M.
  • Risk. None now.
  • Detail. Clients MUST NOT assume any subset or superset relation between challenge scopes and scopes_supported. A step-up in the middle of a pooled session means re-authorizing a connection other callers share.
  • Sources. 2025-11-25/basic/authorization.mdx:103 “MCP servers SHOULD include a scope parameter in the WWW-Authenticate header as defined in”; 2025-11-25/basic/authorization.mdx:541 “#### Step-Up Authorization Flow”; internal/mcpauth/mcpauth.go:69-71

AUTH-15 RFC 9207 iss check on the authorization response

  • What. ASes SHOULD return iss with the authorization code and advertise authorization_response_iss_parameter_supported. Clients MUST validate before redeeming the code: advertised and present, compare exactly; advertised and absent, reject; not advertised but present, compare anyway; neither, proceed.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + med: the mix-up-attack defence, which matters most to a client that talks to many ASes, as a pooling daemon does.
  • Effort. S.
  • Risk. None now.
  • Detail. Plain string comparison: no case folding, default-port or trailing-slash normalisation. Expected to become MUST for ASes later.
  • Sources. 2026-07-28/basic/authorization/index.mdx:196 “On receiving the authorization response, MCP clients MUST apply the validation in [RFC9207 Section 2.4]”

AUTH-16 Fetched metadata issuer must equal the issuer used

  • What. The issuer inside a fetched AS or OIDC metadata document MUST be identical to the issuer identifier the client used to build the well-known URL.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + med: prevents one AS impersonating another through a crafted metadata document.
  • Effort. S.
  • Risk. None now.
  • Detail. Separate from AUTH-15 (which checks the authorization response); both are string-exact.
  • Sources. 2026-07-28/basic/authorization/authorization-server-discovery.mdx:94 “the issuer value in the document MUST be identical to the issuer identifier used to construct the well-known URL”

AUTH-17 Credentials keyed by issuer; re-register on AS change

  • What. Pre-registered or DCR-obtained client credentials MUST be keyed by the AS’s issuer, MUST NOT be reused with another AS, and the client MUST re-register when protected-resource metadata points somewhere new. CIMD ids are portable.
  • Where. 2026-07-28. opencode keys differently: v1 stores tokens and client info per server name in <data>/mcp-auth.json (mode 0600); v2 stores a global Integration credential keyed by sha1(name + url).
  • mcpx @ 05c78b2. No token store exists.
  • Value to mcpx. + med: it fixes the key of mcpx’s future token store as (issuer, client_id), not the server name.
  • Effort. S.
  • Risk. Not done: credential confusion when a server moves AS; opencode’s name- or url-keyed stores would reuse a client id across ASes.
  • Detail. A pre-registered id that does not match the new AS SHOULD surface an error rather than be tried. opencode v2’s comment explains why it keys on name + url: two configs of one URL may hold different accounts. That concern and the issuer rule are compatible: key on issuer, then on account.
  • Sources. 2026-07-28/basic/authorization/client-registration.mdx:186 “keyed by the authorization server’s issuer identifier. When the”; 2026-07-28/basic/authorization/client-registration.mdx:190 “from a different authorization server and MUST re-register”; v1:packages/opencode/src/mcp/auth.ts:37 “const filepath = path.join(Global.Path.data, "mcp-auth.json")”; v2:packages/core/src/mcp/index.ts:165-167

AUTH-18 Refresh-token handling rules

  • What. Clients MUST keep refresh tokens confidential, SHOULD list refresh_token in grant_types, MAY request offline_access when the AS lists it, and MUST NOT assume one is issued; servers SHOULD NOT put offline_access in challenges or scopes_supported.
  • Where. 2026-07-28 (new section). opencode v1 already registers with grant_types: ["authorization_code", "refresh_token"].
  • mcpx @ 05c78b2. Deferred.
  • Value to mcpx. + med: a long-lived daemon lives on refresh tokens.
  • Effort. S within the flow.
  • Risk. Not done: users re-authorize whenever an access token expires.
  • Detail. “Confidential in storage” rules out a world-readable token file; mcpx already restricts its socket with defaults.PrivateMode (internal/daemon/server.go:202), and a token file would need the same.
  • Sources. 2026-07-28/basic/authorization/index.mdx:299 “## Refresh Tokens”; v1:packages/opencode/src/mcp/oauth-provider.ts:48 “grant_types: ["authorization_code", "refresh_token"],”

AUTH-19 oauth-client-credentials extension (machine-to-machine)

  • What. An official extension, io.modelcontextprotocol/oauth-client-credentials, adds the OAuth client-credentials grant (JWT assertion, or client_id + client_secret) so automated systems connect without a user present. Clients declare it in extensions of the per-request clientCapabilities.
  • Where. 2026-07-28 extension (docs/extensions/auth). Never active by default.
  • mcpx @ 05c78b2. Not declared, not performed.
  • Value to mcpx. + med: the extension’s own table lists “background service or daemon” and “CI/CD pipeline” as its users; that is mcpx.
  • Effort. M.
  • Risk. None.
  • Detail. Declaring it is per request, in _meta, like every 2026 capability; a legacy session has no place to declare it.
  • Sources. docs/extensions/auth/oauth-client-credentials.mdx:6 “The OAuth Client Credentials extension (io.modelcontextprotocol/oauth-client-credentials)”; docs/extensions/auth/overview.mdx:54 “| Background service or daemon accessing an MCP server | [OAuth Client Credentials]”; docs/extensions/auth/overview.mdx:63 “they are never active by default.”; docs/extensions/auth/overview.mdx:67

AUTH-20 enterprise-managed-authorization extension (IdP, ID-JAG)

  • What. io.modelcontextprotocol/enterprise-managed-authorization lets an organisation’s IdP govern MCP access: the client exchanges an ID token for an Identity Assertion JWT Authorization Grant (ID-JAG) and trades that for an access token at the MCP server’s AS.
  • Where. 2026-07-28 extension.
  • mcpx @ 05c78b2. n/a.
  • Value to mcpx. − an enterprise IdP flow is out of scope for a local single-user daemon.
  • Effort. L.
  • Risk. None.
  • Detail. Like AUTH-19, never active by default.
  • Sources. docs/extensions/auth/enterprise-managed-authorization.mdx:6 “The Enterprise-Managed Authorization extension (io.modelcontextprotocol/enterprise-managed-authorization)”; docs/extensions/auth/enterprise-managed-authorization.mdx:42

AUTH-21 OAuth redirect listener: fixed port vs ephemeral

  • What. Where the authorization code comes back. opencode v1 listens on a fixed http://127.0.0.1:19876/mcp/oauth/callback (overridable by callbackPort/redirectUri); v2 listens on callback_port, else the configured redirect’s port, else an ephemeral port on 127.0.0.1.
  • Where. opencode v1 and v2 (the spec prescribes no port).
  • mcpx @ 05c78b2. A redirect config field exists (internal/mcpauth/mcpauth.go:75-76); the comment names “a callback listener” as the missing piece (internal/mcpauth/mcpauth.go:62-63).
  • Value to mcpx. + med: a daemon can host the listener itself on its existing loopback server.
  • Effort. S.
  • Risk. A fixed port collides when two clients authorize at once; an ephemeral port conflicts with ASes that require exact redirect URIs.
  • Detail. Config spelling differs: v1 camelCase (callbackPort, redirectUri), v2 snake_case (callback_port, redirect_uri, client_id).
  • Sources. v1:packages/opencode/src/mcp/oauth-provider.ts:11 “const OAUTH_CALLBACK_PORT = 19876”; v1:packages/opencode/src/mcp/oauth-provider.ts:12 “const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"”; v2:packages/core/src/mcp/oauth.ts:353 “server.listen(oauth?.callback_port ?? redirectPort ?? 0, "127.0.0.1", () => {”; v2:packages/schema/src/mcp.ts:42

AUTH-22 Per-server auth: block parsed and never applied

  • What. A server config’s auth: {type: bearer|basic|header|query|env|oauth, …} does nothing. Auth.Resolve(), which would build headers, query parameters and environment, and Auth.Describe() have no non-test caller; only the plain headers: map reaches the HTTP transport.
  • Where. mcpx configuration only.
  • mcpx @ 05c78b2. Auth field at internal/config/config.go:138-141; Resolve at internal/mcpauth/mcpauth.go:93-184; internal/mcpauth is imported only by internal/config for the type; the pool passes p.cfg.Headers (internal/pool/pool.go:377-380).
  • Value to mcpx. + high: ${GITHUB_TOKEN} bearer auth then works as documented.
  • Effort. S.
  • Risk. High if not done: a user who configures auth.bearer sends unauthenticated requests and gets the 401 the code comment promises will never happen.
  • Detail. docs/protocol.md:378 understates it: it says only OAuth is declared and not performed, but every auth type is inert. Describe claims to be “used by mcpx config and mcpx doctor” (internal/mcpauth/mcpauth.go:186-189); neither calls it. The query type also needs the URL rewritten (Resolved.Query), and NeedsOAuth/Missing exist so failures could be reported before a request (internal/mcpauth/mcpauth.go:84-89).
  • Sources. internal/mcpauth/mcpauth.go:93 “func (a *Auth) Resolve() (*Resolved, error) {”; internal/pool/pool.go:379 “Headers: p.cfg.Headers,”; internal/config/config.go:138-141

AUTH-23 /mcp and /v1 authenticate nobody

  • What. Whatever can reach the daemon’s listener can list and call every tool and run scripts. The default bind is loopback, and a warning is printed when binding elsewhere.
  • Where. Every revision says servers SHOULD authenticate all connections (the SSE transport in 2024-11-05, Streamable HTTP after).
  • mcpx @ 05c78b2. No auth middleware; the only wrapper is trackActivity (internal/daemon/server.go:290-293, 344-353), which records time and re-checks config. internal/cli/root.go:196-204 prints the warning. The unix socket relies on filesystem permissions; TCP gets nothing.
  • Value to mcpx. + high: --address 0.0.0.0 is documented (docs/protocol.md:352-355), and even loopback is reachable from a browser because Origin is not checked (transports area; AUTH-24).
  • Effort. M for a bearer token; L for a full OAuth resource server (AUTH-06).
  • Risk. High: remote code execution for anything that reaches the port.
  • Detail. If mcpx adds a 401 to /mcp, opencode v1 will read it as “needs OAuth”: it moves the server to needs_auth and toasts “Run: opencode mcp auth ”, partly by matching “OAuth”/“registration”/“client_id” in error text. Users would need oauth: false and a static header in opencode’s config.
  • Sources. 2024-11-05/basic/transports.mdx:57 “3. Servers SHOULD implement proper authentication for all connections”; 2026-07-28/basic/transports/streamable-http.mdx:65 “Servers SHOULD implement proper authentication”; internal/cli/root.go:202 “the API is unauthenticated”; v1:packages/opencode/src/mcp/index.ts:317

AUTH-24 Code-execution endpoints accept cross-origin simple requests

  • What. The endpoints that run code have no authentication and do not require a JSON content type, so a web page can trigger them. mcpx’s POST /v1/exec decodes JSON from the body whatever its Content-Type, so a cross-origin text/plain “simple” request (no CORS preflight) runs a script with full authority; the page cannot read the answer but the side effect happens. lootbox’s /ws accepts a WebSocket upgrade on the fixed port 9420 with no Origin check: cross-site WebSocket hijacking, and it can read the answer.
  • Where. mcpx and lootbox. opencode and Cloudflare expose no such endpoint.
  • mcpx @ 05c78b2. handleExec calls json.NewDecoder(r.Body).Decode(&req) (internal/daemon/routes_exec.go:212-213); no Origin or Host check in internal/daemon or internal/mcpserver; the API is unauthenticated by design (internal/daemon/server.go:206-208). daemon.port defaults to 0 (ephemeral), so an attacker must find the port first (internal/settings/registry.go:343).
  • Value to mcpx. + high: closes a drive-by remote-code-execution path on the user’s workstation.
  • Effort. S — reject a present non-loopback Origin with 403, require Content-Type: application/json on POST, optionally check Host. Unix-socket and CLI callers send no Origin and are unaffected.
  • Risk. High if not done. Done: a browser UI on another origin needs an allowlist.
  • Detail. The Origin MUST itself (every revision, 403 explicit since 2025-11-25) is recorded in the transports area; this row is the consequence for an unauthenticated server that executes code. lootbox’s own dashboard hard-codes ws://<host>:3000/ws (.lootbox/ui/src/lib/websocket-client.ts:34), so it cannot even reach the :9420 daemon.
  • Sources. internal/daemon/routes_exec.go:212-213; internal/daemon/server.go:206-208; .lootbox/src/lib/rpc/websocket_server.ts:282-290; 2025-11-25/basic/transports.mdx:78 “Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks”

AUTH-25 Scripts see credentials: bindings vs inherited environment

  • What. Cloudflare’s sandbox never holds a credential: connectors are already-authorised RPC bindings, and a search-and-execute server hands the script a request() function, not the token. mcpx scripts inherit the whole environment of whoever runs them, and for /v1/exec that is the daemon’s environment.
  • Where. Cloudflare (bindings); opencode (interpreter, no environment access); lootbox (no --allow-env by default, so the environment exists but cannot be read); mcpx (full os.Environ()).
  • mcpx @ 05c78b2. cmd.Env = append(os.Environ(), "MCPX_RUNTIME="+rt.Name) (internal/runner/runner.go:359); a daemon-run script is a child of the daemon (internal/daemon/routes_exec.go:182-200).
  • Value to mcpx. − full authority is the design (scripts need the filesystem and processes). + one sentence in docs/exec.md is owed: a /v1/exec caller can read every secret in the daemon’s environment.
  • Effort. S — document it, or scrub a configurable denylist from the runner’s environment.
  • Risk. Med: a daemon started with GITHUB_TOKEN and bound beyond loopback exposes the token to any caller (compounded by AUTH-23 and AUTH-24).
  • Detail. Cloudflare’s docs repeat the blog’s “cannot leak keys” claim more cautiously (“never receives the connector credentials or client objects”), so it is documented behaviour, not marketing only.
  • Sources. internal/runner/runner.go:359; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#connectors “The generated code never receives the connector credentials or client objects.”; https://developers.cloudflare.com/agents/model-context-protocol/codemode/#sandbox-and-authorization-boundary “Authentication stays in the host request callback.”

Capabilities

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-11-01T16:20:00-06:00
increment:    1
status:       standard
tags:         area:compare
description:  every ClientCapabilities/ServerCapabilities field per revision; what mcpx, opencode and lootbox declare

A capability is a promise about the revision in force, both as a server to hosts and as a client to upstreams. The named CAP rows were re-audited for #1753; other observations retain their original snapshot.

Status column: CAP-02/04/05/06/08 are current (audited at 2a93f3f64); other rows retain the mcpx @ 05c78b2 snapshot. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpxValueEffortRisk
CAP-01experimental map (client and server); 2026 narrows values to JSON objects2024-11-05 has 2026-07-28 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (JSONObject)n/a — declared in neither direction+ lowSlow
CAP-02client roots; deprecated to a bare {} in 20262024-11-05 has 2026-07-28 deprecates opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep · opencode v1 ✓ v2 ✓ · lootbox —✓ — declares roots and answers from configured roots; relays to the calling host’s roots when declared (modern.go:342-359; daemon/hooks.go:373-409)+ medSmed
CAP-03client roots.listChanged removed in 20262024-11-05 has 2026-07-28 removes mcpx missing 2026-07-28 deprecates24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 rem · opencode —✗ — listChanged:false sent to 2026 upstreams too (#200)+ lowSlow
CAP-04client sampling; structured in 2025-11-25, deprecated in 20262024-11-05 has 2025-11-25 has 2026-07-28 deprecates mcpx has, others don't24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep · opencode v1 — v2 — · lootbox —✓ — declared only with an OnServerRequest handler; the daemon wires its broker handler (modern.go:120-123; daemon/hooks.go:82)+ medSmed
CAP-05client sampling.context gates includeContext2025-11-25 has 2026-07-28 deprecates24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 dep✓ — not declared; undeclared non-none includeContext is dropped before the handler (modern.go:142-179)+ lowSlow
CAP-06client sampling.tools gates tool use inside sampling2025-11-25 has 2026-07-28 deprecates24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 dep✓ — not declared; incoming tools/toolChoice requests are refused with -32602 (modern.go:142-179)− deprecatedMlow
CAP-07client elicitation2025-06-18 has opencode v2 has24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 — v2 ✓ · lootbox —✓ — always declared upstream; host gate checks revision+ highSlow
CAP-08client elicitation.form; {} means form only2025-11-25 has opencode v2 has24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 ✓ · opencode v2 ✓ (applyDefaults)✓ — the host gate accepts form or empty {} and rejects url-only form requests (conn.go:259-285; PR #2162)+ lowSlow
CAP-09client elicitation.url2025-11-25 has mcpx missing opencode v2 has24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 ✓ · opencode v1 — v2 ✓partial — host gate correct; never declared upstream (#200)+ medMmed
CAP-10client tasks (client-hosted tasks), 2025-11-25 only2025-11-25 has 2026-07-28 removes has better replacement24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 rem · opencode — · lootbox —✓ — never declared− one-revisionSlow
CAP-11client extensions map2026-07-28 has26-07 ✓ onlyModern client declares the tasks extension; legacy initialize omits the map (internal/mcpclient/modern.go:capabilities).+ medSlow
CAP-12server logging; deprecated, not removed, in 20262024-11-05 has 2026-07-28 deprecates24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep✓ — never declared; docs/protocol.md says otherwise+ lowSlow
CAP-13server completions2025-03-26 has24-11 — (method ungated) · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — declared in every era mcpx serves+ lowSlow
CAP-14server prompts2024-11-05 has opencode v1 has opencode v2 hasall five ✓ · opencode v1 ✓ v2 ✓ (slash commands)✓ — always declared+ lowSlow
CAP-15server prompts.listChanged; 2026 delivers it only on a listen stream2024-11-05 has 2026-07-28 has mcpx missing opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (listen) · opencode v1 — v2 ✓✗ — stdio declares true, legacy hosts never receive it (#208)+ medSmed
CAP-16server resources2024-11-05 has opencode v1 has opencode v2 hasall five ✓ · opencode v1 ✓ (3 model tools) v2 ✓ (TUI only)✓ — always declared, even with no upstream resources+ medSmed
CAP-17server resources.subscribe; kept in 2026 with a new meaning2024-11-05 has 2026-07-28 has specs conflict mcpx missing24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (= resourceSubscriptions) · opencode never subscribes✗ — stdio declares true; nothing is subscribed upstream (#208)+ medMmed
CAP-18server resources.listChanged; 2026 delivers it only on a listen stream2024-11-05 has 2026-07-28 has mcpx missing opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (listen) · opencode v1 — v2 ✓✗ — stdio declares true, legacy hosts never receive it (#208)+ medSmed
CAP-19server tools2024-11-05 has opencode v1 has opencode v2 hasall five ✓ · opencode v1 ✓ v2 ✓✓ — always declared+ lowSlow
CAP-20server tools.listChanged; 2026 delivers it only on a listen stream2024-11-05 has 2026-07-28 has mcpx missing opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (listen) · opencode v1 ✓ v2 ✓✗ — stdio declares true; mcpx’s own list never changes (#208)+ medSmed
CAP-21server core tasks object, 2025-11-25 only2025-11-25 has 2026-07-28 removes has better replacement24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 rem (extension)Core tasks capability is limited to legacy revision; modern gets extension only (internal/mcpserver/server.go:capabilities).+ medSlow
CAP-22server tasks.list2025-11-25 has 2026-07-28 removes mcpx missing25-11 ✓ onlyLegacy list is declared and lists connection-owned tasks only; unowned tasks remain accessible by ID (internal/mcpserver/server.go:capabilities; internal/mcpserver/tasks.go:handleTask, visible; #1554).+ medSmed
CAP-23server tasks.cancel2025-11-25 has 2026-07-28 removes25-11 ✓ · 26-07 rem (extension implies cancel)Legacy cancel declared and terminal cancel rejected; modern extension acknowledges (internal/mcpserver/server.go:capabilities; internal/mcpserver/tasks.go:handleTask, handleModernTask).+ lowSlow
CAP-24server tasks.requests.tools.call2025-11-25 has 2026-07-28 removes25-11 ✓ onlyLegacy task execution is advertised; eligible gateway tools declare optional support and forbidden tools are rejected (internal/mcpserver/server.go:taskOptional, capabilities; internal/mcpserver/tasks.go:legacyTaskSupport, maybeTask).+ medSlow
CAP-25server extensions map2026-07-28 has26-07 ✓ onlyServer extension map is emitted only for modern peers (internal/mcpserver/server.go:capabilities).+ lowSlow
CAP-26extensions["io.modelcontextprotocol/tasks"] key, both directions2026-07-28 has specs conflict mcpx missing26-07 ext only (SEP-2663 names 2026-06-30)Server and modern client declare the extension; client polls task results, but upstream task input relay remains open (internal/mcpserver/server.go:capabilities; internal/mcpclient/modern.go:capabilities; internal/mcpclient/tasks.go:finishModernTask, TaskInputRequiredError; #1554).+ medMmed

CAP-01 experimental map (client and server)

  • What. Both ClientCapabilities and ServerCapabilities carry experimental, a map of non-standard capability name to settings object. 2026 retypes the values from TypeScript object to JSONObject.
  • Where. Every revision, both sides. Only 2026 constrains the value type.
  • mcpx @ 05c78b2. Declares no experimental key as a server (internal/mcpserver/server.go:1398-1403) or as a client (internal/mcpclient/modern.go:50-56).
  • Value to mcpx. + none on the wire today; any JSON object already satisfied object.
  • Effort. S — type-level only.
  • Risk. None practical either way.
  • Detail. The same JSONObject narrowing applies in 2026 to sampling.context/.tools, elicitation.form/.url, logging and completions. mcpx forwards nothing from experimental in either direction; a host’s experimental capabilities never reach an upstream.
  • Sources. schema/2024-11-05/schema.ts:189 “experimental?: { [key: string]: object };”; schema/2026-07-28/schema.ts:720 “experimental?: { [key: string]: JSONObject };”; schema/2026-07-28/schema.ts:797 “experimental?: { [key: string]: JSONObject };”; internal/mcpclient/modern.go:50-56

CAP-02 client roots

  • What. A client that can answer roots/list declares roots. 2026 keeps the key as a bare {} and deprecates it (SEP-2577); roots then reach a server only as an input request inside an InputRequiredResult.
  • Where. All five revisions; deprecated in 2026. opencode v1 and v2 declare roots: {} and answer one root, the instance or Location directory (v2 notes it is legacy-era only). lootbox declares no capabilities at all, so an upstream roots/list gets the SDK’s method-not-found.
  • mcpx @ 2a93f3f64. As a client it declares roots (internal/mcpclient/modern.go:102-108) and answers roots/list from its configured roots (internal/mcpclient/modern.go:342-359). When a server asks during a call whose originating client declared roots, the daemon relays the request to that client’s roots; otherwise mcpx falls back to its configured roots (internal/daemon/hooks.go:373-409).
  • Value to mcpx. + filesystem servers can use the configured roots or the calling client’s roots, rather than a hard-coded empty list.
  • Effort. S — configured-root fallback and call-scoped host relay are implemented.
  • Risk. low — an empty configured-root list is legal; relayed roots stay scoped to the host call that requested them.
  • Detail. An empty configured-root list remains a legal fallback answer. A relayed answer is scoped to the host whose call raised the request; mcpx does not broadcast one client’s roots to other pooled clients. opencode’s root is the opencode directory, not the worktree. lootbox’s capabilities: {} comes with no request handler at all, so an upstream roots/list or elicitation/create gets the SDK’s default method-not-found.
  • Sources. schema/2026-07-28/schema.ts:732 “roots?: {};”; schema/2026-07-28/schema.ts:724 “@deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).”; internal/mcpclient/modern.go:102-108,342-359; internal/daemon/hooks.go:373-409; v1:packages/opencode/src/mcp/index.ts:46 “roots: {},”; v2:packages/core/src/mcp/client.ts:146; v2:packages/core/src/mcp/client.ts:144 “Legacy era only”; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153

CAP-03 client roots.listChanged

  • What. 2024-11-05 through 2025-11-25 declare roots: { listChanged?: boolean }. 2026 removes the sub-field along with notifications/roots/list_changed; with per-request roots there is nothing to invalidate.
  • Where. Four legacy revisions; absent in 2026. opencode v1 and v2 declare roots without it.
  • mcpx @ 05c78b2. Declares "roots": {"listChanged": false} in every era, including the 2026 io.modelcontextprotocol/clientCapabilities _meta on every modern request (internal/mcpclient/modern.go:52, internal/mcpclient/modern.go:83). A send-conservatively conflict: a key the revision does not define.
  • Value to mcpx. + dropping it for 2026 is the conservative send; nothing is lost.
  • Effort. S — one map per era.
  • Risk. Low: capabilities are an open set, but a strict 2026 server validating roots as {} could reject it.
  • Detail. The same object is sent in the legacy initialize regardless of the negotiated version. Declaring listChanged: false in legacy eras is legal but pointless (mcpx never sends notifications/roots/list_changed). The wire capture of what mcpx declared to an upstream reads {"elicitation":{},"roots":{"listChanged":false},"sampling":{}} (wire, “mcpx as client”).
  • Also recorded from the roots register. listChanged in the four legacy revisions; absent in 2026-07-28. Declares "roots": {"listChanged": false} in every era, including in 2026 per-request capabilities (internal/mcpclient/modern.go:52), a key 2026 does not define. opencode v1 and v2 declare roots: {}. The removal follows from ROOT-02: with MRTR the server asks for roots on every request that needs them, so there is nothing to invalidate. The changelog names the notification’s removal, not the field’s. The wider 2026 per-request capability shape is a capabilities-register item.
  • Sources. schema/2025-11-25/schema.ts:322 “listChanged?: boolean;”; schema/2026-07-28/schema.ts:732 “roots?: {};”; internal/mcpclient/modern.go:52; internal/mcpclient/modern.go:83 “set(MetaClientCapabilities, c.capabilities())”; schema/2026-07-28/schema.ts:724 “@deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).”; v1:packages/opencode/src/mcp/index.ts:46 “roots: {},”.

CAP-04 client sampling

  • What. A client that can run sampling/createMessage declares sampling. It is an opaque object through 2025-06-18; 2025-11-25 gives it context and tools sub-capabilities; 2026 deprecates the whole capability (SEP-2577).
  • Where. All five; structured from 2025-11-25; deprecated 2026. Neither opencode version declares it or registers a handler; lootbox declares nothing.
  • mcpx @ 2a93f3f64. As a client, sampling: {} is declared iff an OnServerRequest handler exists (internal/mcpclient/modern.go:94-123); the daemon wires its broker-backed handler (internal/daemon/hooks.go:82, 155-174). The handler turns requests into broker work; an unanswered request may still expire. As a server, mcpx checks host sampling.tools / sampling.context declarations before forwarding the corresponding shapes (internal/mcpserver/conn.go:561-581,696-703).
  • Value to mcpx. + honesty: servers that have a no-sampling fallback would use it.
  • Effort. S — done; capability declaration is conditional on the configured handler.
  • Risk. If not done: a sampling upstream waits out elicit.ttl (120 s) and then gets an error.
  • Detail. An empty sampling: {} in 2025-11-25+ means “basic sampling, no tools, no context inclusion”. mcpx does not copy host sub-capabilities into its upstream declaration: it refuses incoming tools/toolChoice and strips undeclared non-none includeContext (internal/mcpclient/modern.go:142-179). Host-bound forwarding separately checks the host declarations (SMP-11). mcpx is the only one of mcpx/opencode/lootbox that declares sampling at all.
  • Also recorded from the sampling register. Bare in 2024-11-05..2025-06-18; structured in 2025-11-25 and 2026-07-28. The client declares sampling: {} only when its handler exists (internal/mcpclient/modern.go:120-123); it never declares tools or context. The daemon installs the broker-backed handler (internal/daemon/hooks.go:82).
  • Sources. schema/2024-11-05/schema.ts:202 “sampling?: object;”; schema/2025-11-25/schema.ts:327 “sampling?: {”; schema/2026-07-28/schema.ts:736 “@deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).”; internal/mcpclient/modern.go:94-123; internal/daemon/hooks.go:82,155-174; docs/protocol.md:271 “sampling only when a handler exists to answer it”; v1:packages/opencode/src/mcp/index.ts:42 “// sampling: {},”; v2:packages/core/src/mcp/client.ts:142; schema/2025-06-18/schema.ts:246 “sampling?: object;”

CAP-05 client sampling.context

  • What. Sub-capability that gates includeContext: without it, servers SHOULD use only "none" or omit the field.
  • Where. 2025-11-25 and 2026 (deprecated with the values it gates). The 2025-11-25 changelog does not mention it.
  • mcpx @ 2a93f3f64. Never declared upstream (internal/mcpclient/modern.go:120-123). Incoming non-none includeContext is removed before the handler because mcpx did not declare sampling.context (internal/mcpclient/modern.go:142-179); host-bound forwarding also removes it unless the host declared context (internal/mcpserver/conn.go:696-703).
  • Value to mcpx. + not declaring it is correct: a proxy has no “context from other servers” to inject.
  • Effort. S.
  • Risk. None if left undeclared.
  • Detail. SHOULD-level only: a server may still send thisServer/allServers and the client MAY ignore it.
  • Sources. schema/2025-11-25/schema.ts:332 “context?: object;”; 2025-11-25/client/sampling.mdx:69 “With context inclusion support (soft-deprecated):”; internal/mcpclient/modern.go:120-123,142-179; internal/mcpserver/conn.go:696-703.

CAP-06 client sampling.tools

  • What. Declares that the client accepts tools/toolChoice in sampling/createMessage. Servers MUST NOT send tool-enabled sampling without it; the schema also says the client MUST error if it gets one anyway.
  • Where. 2025-11-25 and 2026 (deprecated).
  • mcpx @ 2a93f3f64. Never declares sampling.tools upstream. A tools/toolChoice request is refused with -32602 before the client handler runs (internal/mcpclient/modern.go:142-179). Host-bound tool requests are offered only to a host that declared sampling.tools (internal/mcpserver/conn.go:572-581).
  • Value to mcpx. − relaying agentic sampling is work for a deprecated feature.
  • Effort. M — the tool loop (tool_use/tool_result blocks) has to pass through unmodified.
  • Risk. Declaring it without a tool loop invites requests mcpx must then refuse.
  • Detail. Prose and schema put the obligation on different sides: the server MUST NOT send, the client MUST error.
  • Sources. schema/2025-11-25/schema.ts:336 “tools?: object;”; 2025-11-25/client/sampling.mdx:40 “Clients MUST declare support for tool use via the sampling.tools capability”; internal/mcpclient/modern.go:142-179; internal/mcpserver/conn.go:313-319,572-581.

CAP-07 client elicitation

  • What. A client that can answer elicitation/create declares elicitation. Opaque in 2025-06-18; structured into modes from 2025-11-25 (CAP-08, CAP-09).
  • Where. 2025-06-18, 2025-11-25, 2026. opencode v1 has it commented out (and answers elicitation/create with -32601); opencode v2 declares it with both modes; lootbox declares nothing.
  • mcpx @ 05c78b2. Always declares "elicitation": {} upstream (internal/mcpclient/modern.go:51); when nobody is listening it answers cancel (internal/mcpclient/modern.go:129-131), which keeps the always-on declaration honest. Asking a host requires the host to be at 2025-06-18 or later and to have declared it (internal/mcpserver/conn.go:192), so mcpx never sends elicitation/create to opencode v1.
  • Value to mcpx. + the broker is built on it.
  • Effort. S.
  • Risk. None.
  • Detail. For opencode v1 users the broker (mcpx elicit) is the only way to answer mcpx’s questions. opencode v2’s handler turns the request into a Location-global form (elicitation register).
  • Sources. schema/2025-06-18/schema.ts:250 “elicitation?: object;”; 2025-06-18/basic/lifecycle.mdx:155 “| Client | elicitation |”; internal/mcpclient/modern.go:51; internal/mcpclient/modern.go:129-131; internal/mcpserver/conn.go:192; v1:packages/opencode/src/mcp/index.ts:44 “// elicitation: {},”; v2:packages/core/src/mcp/client.ts:143

CAP-08 client elicitation.form

  • What. 2025-11-25 splits elicitation into form and url. An empty elicitation: {} is equivalent to { form: {} }; a declaring client MUST support at least one mode; servers MUST NOT send an undeclared mode.
  • Where. 2025-11-25 and 2026. opencode v2 declares form: { applyDefaults: true }.
  • mcpx @ 2a93f3f64. As a client it declares form support (internal/mcpclient/modern.go:98-104). As a server, CanElicit accepts an explicit form key or an empty declaration, but rejects a 2025-11-25+ url-only declaration (internal/mcpserver/conn.go:259-285; PR #2162).
  • Value to mcpx. + low: url-only hosts are rare, but the rule is a MUST NOT.
  • Effort. S — done.
  • Risk. low — url-only hosts are not offered form requests.
  • Detail. applyDefaults is not a schema field (form is an open object) — it is the SDK’s instruction to fill schema defaults into accepted content. Because {} means form-only, mcpx’s upstream declaration never admits url mode (CAP-09).
  • Also recorded from the elicitation register. 2025-11-25 and 2026-07-28. opencode v2 declares { form: { applyDefaults: true }, url: {} }. Downstream, form and url modes are gated independently by the declaration and protocol era (internal/mcpserver/conn.go:259-285). Upstream, mcpx declares form and adds url only when a URL handler exists (internal/mcpclient/modern.go:98-104); the 2026 per-request capabilities carry the same legacy-shaped object (see the capabilities register).
  • Sources. schema/2025-11-25/schema.ts:341 “elicitation?: { form?: object; url?: object };”; 2025-11-25/client/elicitation.mdx:65 “an empty capabilities object is equivalent to declaring support for form mode only”; 2025-11-25/client/elicitation.mdx:75; 2025-11-25/client/elicitation.mdx:77 “Servers MUST NOT send elicitation requests with modes that are not supported by the client.”; internal/mcpserver/conn.go:259-285; v2:packages/core/src/mcp/client.ts:143; internal/mcpclient/modern.go:98-104

CAP-09 client elicitation.url

  • What. Declares support for URL-mode elicitation (the server hands the user a URL to visit; used for OAuth and payment flows).
  • Where. 2025-11-25 and 2026. opencode v2 declares url: {} and also handles notifications/elicitation/complete; opencode v1 has no elicitation at all.
  • mcpx @ 05c78b2. As a server the gate is right: url mode needs a 2025-11-25+ host that declared an explicit url key (internal/mcpserver/conn.go:198-207). As a client mcpx declares bare {} upstream (internal/mcpclient/modern.go:51), yet its broker accepts mode: "url" requests anyway (elicitation register), so a conforming upstream never sends one through mcpx even when the attached host (opencode v2) could open it.
  • Value to mcpx. + OAuth-style and payment flows from remote upstreams would work through mcpx.
  • Effort. M — the declaration has to reflect whether a URL-capable host is attached; 2026’s per-request capabilities allow exactly that, legacy initialize does not.
  • Risk. If declared unconditionally: a URL request with no host able to open it strands the user until the deadline. If not declared: remote servers needing URL flows cannot be used through mcpx.
  • Detail. In 2026 mcpx could declare url only on requests forwarded on behalf of a URL-capable host; in legacy eras the declaration is fixed at initialize for the life of the upstream instance, which may be shared by several hosts.
  • Sources. schema/2026-07-28/schema.ts:771; internal/mcpserver/conn.go:198-207; internal/mcpclient/modern.go:51 “"elicitation": map[string]any{},”; v2:packages/core/src/mcp/client.ts:143; v2:packages/core/src/mcp/client.ts:160

CAP-10 client tasks (client-hosted tasks)

  • What. 2025-11-25 lets a client host tasks for server→client sampling/createMessage and elicitation/create, and declare tasks.list / tasks.cancel. 2026 deletes the field: client-hosted tasks cannot be expressed once unsolicited server→client requests are forbidden.
  • Where. 2025-11-25 only. Neither opencode version declares it (v1 has it commented out, with an issue link); lootbox declares nothing.
  • mcpx @ 05c78b2. Not declared upstream (internal/mcpclient/modern.go:46-58).
  • Value to mcpx. − work for a one-revision feature.
  • Effort. S — leave undeclared.
  • Risk. None.
  • Detail. Sub-fields: list, cancel, requests.sampling.createMessage, requests.elicitation.create. SEP-2663 removes client-hosted tasks because polling a client-hosted task would itself be an unsolicited server→client request. The server-side replacement is the tasks extension (CAP-26).
  • Sources. schema/2025-11-25/schema.ts:346 “tasks?: {”; seps/2663-tasks-extension.md:35 “Client-hosted tasks are no longer expressible.”; v1:packages/opencode/src/mcp/index.ts:48 “// tasks: {},”

CAP-11 client extensions map

  • What. Map of extension identifier to settings object; keys follow the _meta key-naming rules with a mandatory prefix; {} means supported with no settings.
  • Where. 2026 only.
  • mcpx (current). Modern client declares the tasks extension; legacy initialize omits the map (internal/mcpclient/modern.go:capabilities). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + the only way to opt into tasks, apps and auth extensions from 2026 servers.
  • Effort. S to declare; each extension then costs its own implementation.
  • Risk. None for declaring nothing; declaring an extension mcpx cannot handle would be the #177 class.
  • Detail. If one side supports an extension and the other does not, the supporting side MUST revert to core behaviour or reject. The 2026 extensions map has no counterpart in initialize.
  • Sources. schema/2026-07-28/schema.ts:785 “extensions?: { [key: string]: JSONObject };”; 2026-07-28/basic/versioning.mdx:121 “If one party supports an extension but the other does not, the supporting”; internal/mcpclient/modern.go:46-58

CAP-12 server logging

  • What. A server that emits notifications/message declares logging. 2026 deprecates it (SEP-2577) but keeps the field; logging moves to a per-request _meta level (logging register).
  • Where. All five; deprecated in 2026.
  • mcpx @ 05c78b2. Never declared in any revision (internal/mcpserver/server.go:1398; rationale at internal/mcpserver/server.go:566-571; wire W2 shows no logging). That is truthful — mcpx emits no notifications/message. docs/protocol.md:104 claims it is declared for 2025-03-26..2025-11-25 and “removed” in 2026; both halves are wrong against the code and the schema.
  • Value to mcpx. + fix the doc; the code is right.
  • Effort. S.
  • Risk. If the doc stays: operators expect log notifications that never come.
  • Detail. opencode v1 forwards server log messages into its own log; v2 drops them (logging register).
  • Also recorded from the logging register. All five revisions; deprecated (SEP-2577) in 2026-07-28. Never declared in any revision: capabilities() builds only tools, resources, prompts, completions and the task objects (internal/mcpserver/server.go:1398-1403). The logging/setLevel handler explains why: mcpx sends no log messages, so declaring the capability “was a promise of a stream that does not exist” (internal/mcpserver/server.go:566-571). Wire W2 shows no logging. docs/protocol.md:104 still marks logging ✓ for 2025-03-26..2025-11-25 and “removed in 2026-07-28”; both halves are wrong against the code and the schema. The 2026 prose repeats the MUST (“Servers that emit log message notifications MUST declare”), so even a deprecated feature keeps its gate. Capability value is JSONObject in 2026 (was object).
  • Sources. schema/2024-11-05/schema.ts:216 “logging?: object;”; schema/2026-07-28/schema.ts:801 “@deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).”; schema/2026-07-28/schema.ts:808 “logging?: JSONObject;”; internal/mcpserver/server.go:1398; docs/protocol.md:104; 2025-11-25/server/utilities/logging.mdx:19 “Servers that emit log message notifications”; 2026-07-28/server/utilities/logging.mdx:32 “Servers that emit log message notifications”; wire W2.

CAP-13 server completions

  • What. 2025-03-26 adds the completions capability; servers that support completion/complete MUST declare it. 2024-11-05 already had the method, with no capability.
  • Where. Capability in 2025-03-26 and later; method in all five (completion register).
  • mcpx @ 05c78b2. Declares completions: {} in every era it serves (internal/mcpserver/server.go:1402; wire W2), and answers the method in every era. As a client it calls completion/complete only if the upstream declared completions (internal/mcpclient/request.go:50).
  • Value to mcpx. + low; the declaration is correct. What the answers contain is the completion register’s concern.
  • Effort. S.
  • Risk. None.
  • Detail. Unsupported → -32601 from 2025-03-26 prose. mcpx does not serve 2024-11-05, so the missing capability in that revision matters only on the client side (a 2024-11-05 upstream’s completions are unreachable).
  • Sources. schema/2025-03-26/schema.ts:240 “completions?: object;”; 2025-03-26/server/utilities/completion.mdx:24 “Servers that support completions MUST declare the completions capability:”; internal/mcpserver/server.go:1402; internal/mcpclient/request.go:50

CAP-14 server prompts

  • What. A server that offers prompts declares prompts (with optional listChanged, CAP-15).
  • Where. All five. opencode v1 and v2 list prompts only from servers that declare prompts and turn each into a slash command.
  • mcpx @ 05c78b2. Always declared (internal/mcpserver/server.go:1401), whether or not any upstream has prompts.
  • Value to mcpx. + low: an empty prompt list costs a host one cheap request.
  • Effort. S.
  • Risk. None.
  • Detail. Unlike resources (CAP-16), declaring prompts costs opencode no model context: prompts become commands, not tools.
  • Sources. schema/2024-11-05/schema.ts:220 “prompts?: {”; internal/mcpserver/server.go:1401; v1:packages/opencode/src/command/index.ts:105; v2:packages/core/src/plugin/command.ts:55

CAP-15 server prompts.listChanged

  • What. Until 2025-11-25, listChanged: true means the server may push notifications/prompts/list_changed unsolicited. In 2026 it means the server SHOULD send it to clients that opened subscriptions/listen with promptsListChanged.
  • Where. All five; delivery model changes in 2026. opencode v2 refetches prompts (and republishes the commands) on the notification; v1 ignores it but fetches prompts fresh on each command listing.
  • mcpx @ 05c78b2. Declared as “this connection can push” (internal/mcpserver/server.go:1390): true on stdio (wire S2), false over HTTP (wire W2), because HTTP Conns have no push function (internal/mcpserver/conn.go:87-92). On a legacy stdio connection nothing starts the notifier — restartListen is called only from resources/subscribe and subscriptions/listen (internal/mcpserver/server.go:619, internal/mcpserver/server.go:636) — so the true flag is never delivered. The comment at internal/mcpserver/server.go:1393-1396 claims the opposite.
  • Value to mcpx. + legacy hosts would see upstream prompts appear and disappear; the daemon’s event bus already sees those changes (internal/daemon/hooks.go:37-51).
  • Effort. S — start a listener for the three list kinds on a legacy stdio connection after initialized.
  • Risk. If not done: a declared-but-not-delivered capability (#177 class). Over HTTP the flag is honest but no host ever learns of a change: mcpx offers no legacy GET stream and answers 2026 subscriptions/listen over HTTP with an empty 202 (transports register).
  • Detail. The 2025-11-25 schema says list_changed “may be issued by servers without any previous subscription”; 2026 ties it to an opted-in stream and requires _meta["io.modelcontextprotocol/subscriptionId"] on every such notification (notifications register). What changes is the upstreams’ lists surfaced through mcpx’s own prompts/list.
  • Sources. schema/2026-07-28/schema.ts:829 “listChanged?: boolean;”; schema/2025-11-25/schema.ts:1159 “This may be issued by servers without any previous subscription from the client.”; 2026-07-28/server/prompts.mdx:169; internal/mcpserver/server.go:1390 “push := s.Notify != nil && c != nil && c.canPush()”; internal/mcpserver/server.go:1393-1396 “are defined in every revision mcpx serves and arrive unsolicited on a”; v2:packages/core/src/mcp/index.ts:374

CAP-16 server resources

  • What. A server that offers resources declares resources (with optional subscribe and listChanged).
  • Where. All five. opencode v1 adds three model-facing tools (list_mcp_resources, list_mcp_resource_templates, read_mcp_resource) to every request as soon as any connected server declares resources, even in code mode; v2 uses the capability only for the TUI @ list.
  • mcpx @ 05c78b2. Always declared (internal/mcpserver/server.go:1400), even when no upstream has resources.
  • Value to mcpx. + three tool schemas on every opencode v1 request is exactly the context cost mcpx exists to avoid; − mcpx always has at least its own artifact template (mcpx://artifacts/{id}, wire W18), so dropping the capability also drops artifact reads for that host. A per-host choice, not a simple removal.
  • Effort. S.
  • Risk. If not done: every opencode v1 session pays the three schemas. If done unconditionally: hosts lose resources/read of script artifacts.
  • Detail. v1 gates resource reads behind a read permission with pattern mcp:<server>:<uri>. How resources reach the model in each product is RES-16.
  • Sources. schema/2024-11-05/schema.ts:229 “resources?: {”; internal/mcpserver/server.go:1400; v1:packages/opencode/src/session/tools.ts:136; v2:packages/tui/src/component/prompt/autocomplete.tsx:413

CAP-17 server resources.subscribe

  • What. Legacy: the server supports resources/subscribe. 2026 removes that RPC but keeps the flag, redefined as “supports resourceSubscriptions on subscriptions/listen”.
  • Where. All five; meaning changes in 2026. Neither opencode version ever subscribes.
  • mcpx @ 05c78b2. subscribe: push (internal/mcpserver/server.go:1400): true on stdio (wire S2), false over HTTP (wire W2). resources/subscribe is accepted in every era but nothing subscribes upstream (SUB-15), so on stdio the flag is declared and not delivered.
  • Value to mcpx. + real resource watches (files, tickets) through mcpx.
  • Effort. M — needs upstream subscription on legacy servers and a listen stream on modern ones.
  • Risk. If not done: a host subscribes and waits for updates that arrive only if an upstream sends them unasked. A client reading the 2026 flag as “call resources/subscribe” gets -32601 from a pure-2026 server.
  • Detail. The 2026 changelog says the RPC was replaced but never says the flag was kept and redefined — the prose (resources.mdx) and schema do. On stdio, mcpx maps a subscribe onto restartListen with only the URI list, which replaces any active list-changed filter on that connection (internal/mcpserver/server.go:619).
  • Also recorded from the notifications register. The flag is in all five revisions; its meaning changes in 2026-07-28, which the changelog does not say. Declares subscribe: push in every era (internal/mcpserver/server.go:1400), true on stdio and false over HTTP. Updates reach a subscriber only if an upstream sends them unasked, because mcpx never subscribes upstream (SUB-15; the resources register has the details).
  • Sources. schema/2026-07-28/schema.ts:850 “subscribe?: boolean;”; 2026-07-28/server/resources.mdx:58 “- subscribe : whether the server supports resource-specific update notifications”; 2026-07-28/changelog.mdx:18; internal/mcpserver/server.go:1400; internal/mcpserver/server.go:619

CAP-18 server resources.listChanged

  • What. As CAP-15, for notifications/resources/list_changed; in 2026 opted into with resourcesListChanged.
  • Where. All five; listen-only in 2026. opencode v2 publishes a ResourcesChanged event on it; v1 ignores it.
  • mcpx @ 05c78b2. Same as CAP-15: true on stdio and never delivered to legacy hosts, false over HTTP (internal/mcpserver/server.go:1400, internal/mcpserver/server.go:1390).
  • Value to mcpx. + hosts learn when an upstream adds resources.
  • Effort. S (shares CAP-15’s fix).
  • Risk. As CAP-15.
  • Detail. mcpx’s pool does drop its cached schema on an upstream list_changed and publishes an event (internal/daemon/hooks.go:37-51); only the last hop to the host is missing.
  • Sources. schema/2026-07-28/schema.ts:854 “listChanged?: boolean;”; 2026-07-28/server/resources.mdx:234; internal/mcpserver/server.go:1400 “"resources": map[string]any{"subscribe": push, "listChanged": streamed},”; v2:packages/core/src/mcp/index.ts:375

CAP-19 server tools

  • What. A server that offers tools declares tools.
  • Where. All five. opencode v1 registers its tools/list_changed handler only when the server declares tools.
  • mcpx @ 05c78b2. Always declared (internal/mcpserver/server.go:1399).
  • Value to mcpx. + low; nothing to change.
  • Effort. S.
  • Risk. None.
  • Detail. mcpx’s tool list is its own fixed surface plus generated op tools (tools register), not the upstreams’ tools.
  • Sources. schema/2024-11-05/schema.ts:242 “tools?: {”; internal/mcpserver/server.go:1399; v1:packages/opencode/src/mcp/index.ts:462

CAP-20 server tools.listChanged

  • What. As CAP-15, for notifications/tools/list_changed; in 2026 opted into with toolsListChanged.
  • Where. All five; listen-only in 2026. Both opencode versions refetch tools on it (v2 debounces the registry reconcile by 100 ms; on a modern connection v2’s SDK opens subscriptions/listen itself).
  • mcpx @ 05c78b2. Same declaration rule as CAP-15 (internal/mcpserver/server.go:1399). Independently of delivery, mcpx’s own tool list cannot change: the MCP surface is built once per daemon under a sync.Once (internal/cli/serve_ask.go:242), so adapters or APIs added by a config reload never appear.
  • Value to mcpx. + adapters added at runtime would reach hosts without a restart.
  • Effort. S for delivery; S to rebuild the surface on reload.
  • Risk. If not done: users restart the daemon to see new adapter tools; the declared flag promises something that cannot happen.
  • Detail. tools/list_changed is the only list-changed kind both opencode versions honour, so it is the one that matters most for mcpx’s primary host.
  • Also recorded from the notifications register. Legacy revisions (2024-11-05..2025-11-25) over stdio. Capabilities are true when the connection can push (internal/mcpserver/server.go:1390, :1397-1402; wire S2). restartListen is called only from resources/subscribe and subscriptions/listen (internal/mcpserver/server.go:619, :636), and ServeStdio starts nothing (internal/mcpserver/server.go:1000-1073). The capability comment claims list_changed notifications “arrive unsolicited on a connection that can push” (internal/mcpserver/server.go:1393-1396). Over HTTP the flags are truthfully false (no push), and docs/protocol.md:100-102 claims ✓ everywhere. A send-conservatively conflict: a capability promising a stream that does not exist. Legacy HTTP hosts could have had these on the GET stream, which mcpx answers with 405 (a transports-register item).
  • Sources. schema/2026-07-28/schema.ts:869 “listChanged?: boolean;”; 2026-07-28/server/tools.mdx:238 “When the list of available tools changes, servers that declared the listChanged”; internal/cli/serve_ask.go:242 “l.once.Do(func() { l.srv, l.err = l.app.MCPServer(ctx) })”; v1:packages/opencode/src/mcp/index.ts:462; v2:packages/core/src/tool/mcp.ts:140; internal/mcpserver/server.go:1390 “push := s.Notify != nil && c != nil && c.canPush()”; internal/mcpserver/server.go:1393-1396 “arrive unsolicited on a”; internal/mcpserver/server.go:619; docs/protocol.md:100; wire S2.

CAP-21 server core tasks object

  • What. 2025-11-25 servers declare task support in capabilities.tasks (list, cancel, requests.tools.call). 2026 moves tasks into the io.modelcontextprotocol/tasks extension, and SEP-2663 says servers MUST NOT keep advertising the legacy capabilities under a protocol version that includes the extension.
  • Where. 2025-11-25 only; replaced by the extension in 2026.
  • mcpx (current). Core tasks capability is limited to legacy revision; modern gets extension only (internal/mcpserver/server.go:capabilities). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + keeps the capability object honest to modern hosts.
  • Effort. S — add a ceiling for the core shape.
  • Risk. Low for parsing (open set), but the declaration promises tasks/list and tasks/result, which the extension removed.
  • Detail. The comment above the branch says the capability is “declared both ways and a client of either era finds it where it looks”; SEP-2663 forbids exactly that. The shape of what mcpx actually returns for tasks is in the tasks register.
  • Sources. internal/mcpserver/server.go:1411; internal/mcpserver/server.go:1406 “declared both ways and a client of either era finds it where it”; seps/2663-tasks-extension.md:949 “MUST NOT continue to advertise the legacy capabilities under any protocol version that includes this extension”; 2026-07-28/changelog.mdx:22; schema/2025-11-25/schema.ts:433 “tasks?: {”

CAP-22 server tasks.list

  • What. Declares tasks/list. A server that cannot bind tasks to requestors SHOULD NOT declare it.
  • Where. 2025-11-25; removed with the extension (which has no tasks/list).
  • mcpx (current). Legacy list is declared and lists connection-owned tasks only; unowned tasks remain accessible by ID (internal/mcpserver/server.go:capabilities; internal/mcpserver/tasks.go:handleTask, visible; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + removes cross-host metadata exposure on a shared daemon.
  • Effort. S — stop declaring, or bind tasks to a requestor.
  • Risk. If not done: any host sees every host’s task ids and can collect their results.
  • Detail. Task ids are 64 random bits (internal/tasks/tasks.go:86-90), “cryptographically secure” only weakly.
  • Sources. schema/2025-11-25/schema.ts:437 “list?: object;”; 2025-11-25/basic/utilities/tasks.mdx:876 “SHOULD NOT declare the tasks.list capability”; internal/mcpserver/server.go:1412

CAP-23 server tasks.cancel

  • What. Declares tasks/cancel. In 2025-11-25 cancelling a terminal task MUST be rejected with -32602; the 2026 extension implies cancel and answers with an empty acknowledgement.
  • Where. 2025-11-25; implied by the extension in 2026.
  • mcpx (current). Legacy cancel declared and terminal cancel rejected; modern extension acknowledges (internal/mcpserver/server.go:capabilities; internal/mcpserver/tasks.go:handleTask, handleModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. The declaration itself is fine for 2025-11-25; the behaviour behind it is the gap.
  • Sources. schema/2025-11-25/schema.ts:441 “cancel?: object;”; 2025-11-25/basic/utilities/tasks.mdx:494; internal/mcpserver/tasks.go:124 “return reply(snap)”

CAP-24 server tasks.requests.tools.call

  • What. Declares that tools/call may be task-augmented. Each tool must also opt in with execution.taskSupport; absent means "forbidden", and a conforming client then MUST NOT request a task.
  • Where. 2025-11-25 only.
  • mcpx (current). Legacy task execution is advertised; eligible gateway tools declare optional support and forbidden tools are rejected (internal/mcpserver/server.go:taskOptional, capabilities; internal/mcpserver/tasks.go:legacyTaskSupport, maybeTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + long mcpx_exec/mcpx_call runs could survive disconnects for conforming hosts.
  • Effort. S — add execution: {taskSupport: "optional"} to long-running tools, at 2025-11-25 only.
  • Risk. If not done: declared but unreachable (#177 class). If done carelessly: taskSupport: "required" would make the tool uncallable from opencode v1, whose SDK throws on it (TOOL-21).
  • Detail. execution must not be sent below 2025-11-25, and tools/list is not revision-filtered today (TOOL-25).
  • Also recorded from the tasks register. 2025-11-25 only; the extension has no per-tool gate. Declares tasks.requests.tools.call (internal/mcpserver/server.go:1413), but its Tool type has no execution field (internal/mcpserver/server.go:260-268), so every tool is forbidden and a conforming client never asks for a task. mcpx still accepts a task param from anyone and creates the task (internal/mcpserver/server.go:477-501; wire W22) rather than -32601. The field must not be sent below 2025-11-25 (send conservatively). The tools register owns Tool.execution itself.
  • Sources. schema/2025-11-25/schema.ts:453 “call?: object;”; 2025-11-25/basic/utilities/tasks.mdx:115 “clients MUST NOT attempt to invoke the tool as a task”; internal/mcpserver/server.go:1413; internal/mcpserver/server.go:260-268; schema/2025-11-25/schema.ts:1270; wire W22.

CAP-25 server extensions map

  • What. Servers advertise extensions in an extensions map returned by server/discover.
  • Where. 2026 only; ServerCapabilities in 2025-11-25 has no such member.
  • mcpx (current). Server extension map is emitted only for modern peers (internal/mcpserver/server.go:capabilities). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low; a conservative send.
  • Effort. S — gate on 2026.
  • Risk. Low (open set).
  • Detail. The only key mcpx puts in it is the tasks extension (CAP-26).
  • Sources. schema/2026-07-28/schema.ts:882 “extensions?: { [key: string]: JSONObject };”; 2026-07-28/changelog.mdx:32; internal/mcpserver/server.go:1415; docs/protocol.md:93-94; docs/protocol.md:106

CAP-26 extensions["io.modelcontextprotocol/tasks"] key

  • What. The 2026 way to negotiate tasks: the server advertises the key via server/discover, the client declares it in each request’s capabilities. It is not wire-compatible with 2025-11-25 tasks.
  • Where. 2026, as an official extension (SEP-2663). Under 2025-11-25, servers MUST NOT treat the capability as enabling tasks.
  • mcpx (current). Server and modern client declare the extension; client polls task results, but upstream task input relay remains open (internal/mcpserver/server.go:capabilities; internal/mcpclient/modern.go:capabilities; internal/mcpclient/tasks.go:finishModernTask, TaskInputRequiredError; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + long-running upstream jobs through modern servers; honest advertising to modern hosts.
  • Effort. M — the extension’s shapes (resultType: "task", tasks/update, DetailedTask) differ from 2025-11-25.
  • Risk. If not done: 2026 hosts are promised an extension whose wire format mcpx does not speak.
  • Detail. SEP-2663 names the protocol version 2026-06-30 in its compatibility table, not 2026-07-28; the local clone has no other source for the extension (the normative text lives in an external ext-tasks repo). Its backward-compatibility table is the only place that says servers MUST migrate from the legacy capabilities.
  • Also recorded from the tasks register. 2025-11-25 core; 2026-07-28 extension. Declares both, to both eras (internal/mcpserver/server.go:1404-1417); the code comment says so on purpose: “declared both ways and a client of either era finds it where it looks” (internal/mcpserver/server.go:1405-1407). As a client of modern upstreams it never declares the extension. No extension-specific settings are defined yet; The doc repeats the table (docs/protocol.md:106) against its own rule that nothing is declared outside the revision that defines it.
  • Sources. seps/2663-tasks-extension.md:47; seps/2663-tasks-extension.md:948 “This extension is not defined under the 2025-11-25 protocol version.”; seps/2663-tasks-extension.md:949; internal/mcpserver/server.go:1415; 2025-11-25/basic/utilities/tasks.mdx:45; schema/2025-11-25/schema.ts:433; seps/2663-tasks-extension.md:83 “No extension-specific settings are currently defined; an empty object indicates support.”; internal/mcpserver/server.go:1404-1417; docs/protocol.md:106.

Code mode and script execution

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Every difference in how lootbox, mcpx, opencode code mode (v1, v2) and Cloudflare code mode run model-written code against MCP tools.

Code mode means the model writes a program that calls MCP tools as typed functions, and only the program’s output goes back into context. The products agree on that idea and differ on engine, sandbox, limits, how results and failures come back, how tools are discovered, and whether anything outside a model turn can run it. For mcpx, two things matter most: it has no sandbox, on purpose, which the MCP client guide treats as a precondition (CODE-10); and where others already do something mcpx needs, the gaps are small and concrete: an output cap, typed failures, outputSchema return types, reserved-name checks. Differences whose area another register owns (result unwrapping, the opencode plugin’s mcpx_exec, pools, credentials) are referred to by that register’s ID; in the Where column v1/v2 are opencode 1.18.31/2.0.3, CF is Cloudflare, and lootbox is the deployed build unless a row says “fork” (CODE-01).

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
CODE-01Deployed lootbox is upstream 587a5a1 plus four patches, not the forklootbox hasdeployed: upstream + 4 patches · .lootbox/: fork 887db38n/a — decides which lootbox the other rows describe+ lowSlow
CODE-02Real runtime chosen per run: deno, then bun, then nodemcpx has, others don'tmcpx 3 runtimes · lootbox deno only · v1/v2 interpreter · CF isolate✓ — script.runtime auto, deno, bun or node+ medSlow
CODE-03opencode runs a confined interpreter with no ambient APIsopencode v1 has opencode v2 has does not match mcpx's goalv1 ✓ · v2 ✓ · lootbox, mcpx, CF run real enginesn/a — real runtime with full authority, by choice− authorityXLlow
CODE-04Cloudflare runs each execution pass in a fresh V8 isolatedoes not match mcpx's goalCF ✓ · others ✗n/a — one OS process group per run− workerdXLlow
CODE-05opencode supports only a subset of JavaScriptopencode v1 has opencode v2 hasv1 no classes or generators · v2 no classes · others full✓ — full TypeScript on a real runtime+ lowSlow
CODE-06Script type-checked against generated types before it runslootbox haslootbox on by default · mcpx opt-in · v1, v2, CF neverpartial — script.typecheck exists, defaults to off+ medSmed
CODE-07TypeScript stripped without checking; v2 workerd build skips transpileopencode v1 has opencode v2 hasv1 ✓ · v2 ✓ (workerd: JS only) · CF JS onlyn/a — the real runtime parses TypeScript+ lowSlow
CODE-08Default script permissions: --allow-net versus --allow-alllootbox haslootbox net only · mcpx all · v1/v2, CF no ambient access✓ — --allow-all by design; net, strict profiles exist− policySmed
CODE-09Cloudflare blocks or proxies all outbound networkdoes not match mcpx's goalCF ✓ · v1/v2 no network API · lootbox, mcpx openn/a — no egress control beyond Deno strict− authorityLlow
CODE-10MCP client guide: programmatic tool calling requires a sandboxdoes not match mcpx's goalguide (every docs version) · lootbox, v1, v2, CF sandbox · mcpx nonen/a — no sandbox, deliberately− goalLmed
CODE-11Default whole-script wall-clock timeoutlootbox has opencode v1 has opencode v2 haslootbox 10 s · mcpx 120 s · v1/v2 none · CF 60 s or 30 s✓ — exec.timeout 120 s, then process-group kill+ lowSlow
CODE-12Per-tool-call timeout inside a scriptlootbox has opencode v1 has opencode v2 haslootbox 30 s · mcpx 120 s · v1 SDK 60 s · v2 12 h✓ — pool.callTimeout 120 s+ lowSmed
CODE-13Progress token requested so long calls outlive the timeoutopencode v1 has opencode v2 has mcpx missingv1 token and reset · v2 token, no reset · lootbox, mcpx nonepartial — sends a token when the host did and relays progress (#212); the daemon’s own timeout does not reset on it+ medSmed
CODE-14CPU-time and subrequest caps per invocationdoes not match mcpx's goalCF cpuMs, subRequests · others wall clock onlyn/a — wall clock only− marginalMlow
CODE-15Tool-call budget per executionopencode v1 has opencode v2 hasv1/v2 knob, unset by host · lootbox, mcpx, CF none✗ — only the timeout bounds a looping script+ lowSlow
CODE-16Cap on the size of returned outputopencode v1 has opencode v2 has mcpx hasv1/v2 knob · CF 6,000 tokens · lootbox whole stdout, mcpx capped at exec.maxOutputBytes✓ — returned stdout capped at the execsvc Result boundary (default 64KiB): head kept, in-band marker names dropped bytes, stdoutTruncated set; full record unchanged (#1937)+ highSmed
CODE-17Concurrent tool calls within one programopencode v1 hasv1 8 · v2 unrestricted · lootbox, mcpx, CF no capn/a — per-server pool max 4, no per-run cap+ lowSlow
CODE-18Upstream call cancelled when the script times out or dieshas better replacementmcpx ✓ · v1/v2 ✓ · lootbox ✗ · CF unverified✓ — context cancel sends notifications/cancelled+ lowSlow
CODE-19Final top-level expression becomes the resultopencode v2 has mcpx missingv2 ✓ · CF ✓ · v1 null · lootbox none · mcpx stdout✗ — result is stdout, emit() or a default export (#216)+ medMlow
CODE-20What happens to un-awaited calls when the program endsopencode v1 has opencode v2 hasv1 awaited · v2 interrupted · lootbox, mcpx run onn/a — the runtime’s event loop finishes them+ lowSlow
CODE-21Working directory the script runs inlootbox has has better replacementlootbox daemon’s cwd · mcpx caller’s cwd · v1, v2, CF none✓ — caller’s cwd; /v1/exec takes cwd+ lowSlow
CODE-22Caller chooses local or remote executionmcpx has, others don'tmcpx ✓ · lootbox always daemon · v1/v2 in-process · CF platform✓ — exec.where auto, local or remote+ medSlow
CODE-23How arguments reach a scriptlootbox has has better replacementlootbox stdin only · mcpx argv, exports, @param · CF one input✓ — argv, default export, --export, placeholders+ lowSlow
CODE-24Where named scripts are looked uplootbox has has better replacementlootbox one dir · mcpx upward search · CF snippets · v1/v2 none✓ — .mcpx/scripts upward, then user dir+ lowSlow
CODE-25Named injection points in the script launchermcpx has, others don'tmcpx ✓ · others wrap code, no hooks✓ — before, prefix, onSuccess, onError, suffix+ lowSlow
CODE-26emit() streams values out while the script runsmcpx has, others don'tmcpx ✓ · others return at the end✓ — SSE or NDJSON frames from /v1/exec+ medSlow
CODE-27Structured log records on a side channelmcpx has, others don'tmcpx records · v1/v2, CF logs: string[] · lootbox raw stderr✓ — log.* behind a sentinel, parsed into logs[]+ lowSlow
CODE-28lootbox drops stderr on success and stdout on failurehas better replacementlootbox ✗ · mcpx, v1/v2, CF keep both✓ — keeps both; stderr tail-bounded+ lowSlow
CODE-29One JSON document per runhas better replacementmcpx ✓ · v1/v2 internal · CF three variants · lootbox text✓ — --json run, /v1/exec, mcpx_exec+ lowSlow
CODE-30Failures reported as typed kindsopencode v1 has opencode v2 has mcpx missingv1/v2 kind · CF status: "error" · mcpx, lootbox strings✗ — error string, exitCode, timedOut only (#216)+ medMlow
CODE-31Budgeted catalogue: every namespace, signatures to about 2,000 tokensopencode v1 has opencode v2 hasmcpx ✓ · v1 ✓ · v2 ✓ · lootbox counts only · CF all or none✓ — mcpx catalog, mcpx_catalog, 2,000-token default+ lowSlow
CODE-32Catalogue changes sent as a delta when shorteropencode v2 hasv2 ✓ · mcpx machinery ✓ · v1 full re-render✓ — Fingerprint, Diff, ShorterOf exist+ lowSlow
CODE-33Pinned tools always shown in the catalogueopencode v2 hasv2 ✓ · mcpx --bias only · others ✗✗ — bias scoring favours terms; nothing is pinned+ lowSlow
CODE-34Tool search callable from inside the programopencode v1 has opencode v2 hasmcpx sync · v1 async · v2 sync · CF async · lootbox ✗✓ — synchronous search() and describe()+ lowSlow
CODE-35In-program search pages its resultsopencode v1 has opencode v2 has mcpx missingv1/v2 offset, next · CF truncated · mcpx limit only✗ — search(query, limit = 20), no offset (#216)+ lowSlow
CODE-36In-program search also finds saved snippetsmcpx missingCF ✓ · mcpx recipes outside scripts · others ✗✗ — in-script search() covers tools only (#216)+ lowMlow
CODE-37Approval pauses a run by aborting it and replaying itdoes not match mcpx's goalCF ✓ · v1/v2 ask per call · mcpx elicitation · lootbox ✗n/a — elicitation blocks in-process; no replay− determinismXLlow
CODE-38Rollback through a per-tool revertdoes not match mcpx's goalCF ✓ · others ✗n/a — MCP tools have no revert− no-revertXLlow
CODE-39Only the host may promote a run to a saved snippetdoes not match mcpx's goalCF host only · lootbox, mcpx agent writes files · v1/v2 nonen/a — agents write .mcpx/scripts/ directly− workflowMmed
CODE-40Tool names that are JavaScript reserved words get a suffixmcpx missingCF ✓ · mcpx, lootbox ✗ · v1/v2 bracket notation✗ — mcpx types prints function delete(…) (#215)+ lowSlow
CODE-41Namespace names checked against reserved words and prelude namesmcpx missingCF ✓ · lootbox mcp_ prefix · v1/v2 brackets · mcpx ✗✗ — a server named log may break every script (#215)+ medShigh
CODE-42Two tool names that sanitise to the same identifiermcpx missingCF throws · mcpx last wins · lootbox first wins✗ — duplicate keys; wrong tool called silently (#215)+ medSmed
CODE-43opencode keeps real tool names and uses bracket notationopencode v1 has opencode v2 hasv1/v2 ✓ · mcpx, lootbox, CF renamen/a — renames fancy-name to fancy_name+ lowMlow
CODE-44Return types generated from a tool’s outputSchemaopencode v1 has opencode v2 has 2025-06-18 has mcpx missingv1/v2 ✓ · CF field · mcpx any · lootbox envelope✗ — parses outputSchema; every result typed any (#216)+ medSlow
CODE-45lootbox’s generated types carry no descriptionshas better replacementlootbox ✗ · mcpx, v1/v2, CF ✓✓ — JSDoc for tools and properties+ lowSlow
CODE-46lootbox generates one function per MCP resourcelootbox haslootbox ✓ · mcpx generic reader · v1/v2 none · CF undocumentedn/a — one readResource(uri)− bloatSlow
CODE-47lootbox exposes local TypeScript files as namespaceslootbox has has better replacementlootbox ✓ · mcpx adapters and importsn/a — a plain module import instead, by choice− costMlow
CODE-48lootbox YAML prompt workflowslootbox has does not match mcpx's goallootbox ✓ · others ✗n/a — left out on purpose− scopeMlow
CODE-49lootbox’s script-to-gateway protocol is its own WebSocket JSONlootbox has has better replacementlootbox ✓ · mcpx REST and MCP · v1/v2 in-process · CF RPC✓ — documented REST /v1/call, /v1/exec+ lowSlow
CODE-50Script client reaches the daemon over a unix socketmcpx has, others don'tmcpx deno/bun socket, node TCP · others ✗✓ — socket mode is the access control+ medSlow
CODE-51Cloudflare connects script and host through Workers RPCdoes not match mcpx's goalCF ✓ · others ✗n/a — HTTP to the daemon− Workers-onlyXLlow
CODE-52Every run recorded with its script text and outputlootbox haslootbox SQLite · mcpx log without source · CF last 50 · v1/v2 transcriptpartial — run ids and log records; no script source+ lowMmed
CODE-53opencode wraps MCP servers in code mode by defaultopencode v1 has opencode v2 has mcpx missingv2 default, per server · v1 behind a flag this build sets · opt-out only for executor✗ — docs never tell users to opt out (#218)+ highSmed
CODE-54Code execution reachable from a shell, HTTP or CIlootbox hasmcpx ✓ · lootbox CLI and WS · CF Worker · v1/v2 model turn only✓ — mcpx exec, /v1/exec, mcpx_exec+ highSlow

CODE-01 The deployed lootbox is upstream 587a5a1 plus four patches, not the fork

  • What. The daemon answering on 127.0.0.1:9420 is ~/.local/bin/lootbox, built by lootbox-update from upstream jx-codes/lootbox@587a5a1 plus four patches. The .lootbox/ checkout cited throughout this register is the dezren39 fork (branch configurable-timeout, HEAD 887db38), 51 commits ahead of that upstream.
  • Where. Deployed: upstream plus lootbox-deno-2.9, lootbox-global-config, lootbox-loopback and lootbox-ui-dir. The fork is not deployed.
  • mcpx @ 05c78b2. n/a. ASSESSMENT.md audited “jx-codes/lootbox (and the dezren39 fork)” against the live installation (ASSESSMENT.md:3-4) and counts the fork 51 commits ahead (ASSESSMENT.md:9-10).
  • Value to mcpx. + low: every lootbox cell in this register is only as accurate as the build it describes.
  • Effort. S — a note; no code.
  • Risk. Reading a fork-only feature (the health monitor, per-session) as behaviour of the running daemon.
  • Detail. Fork-only: the health monitor, the multi-client strategies (warn/fail/auto-port/per-session), configurable timeout and permissions, the deep /health, and lootbox health. The live /health returned {"status":"ok"}, not the fork’s subsystem report, which confirms the upstream build is what runs. The core code-mode path (parse_mcp_schemas.ts, mcp_schema_fetcher.ts) is the same in both; git diff --stat 587a5a1 HEAD does not list those files. Upstream hard-codes what the fork makes configurable: git show 587a5a1:src/lib/execute_llm_script.ts has a 10-second script timeout (line 26) and --allow-net, --allow-import=localhost:<port>, --reload=…/client.ts, --no-check=remote (lines 31-34), and its src/lib/rpc/execute_mcp.ts hard-codes a 30-second per-call timeout (line 60), so CODE-06, CODE-08, CODE-11 and CODE-12 hold for both. Rows about fork-only process behaviour (startup pings, multi-instance strategies) are in the process-model register (PM-22, PM-24).
  • Sources. nix:flake.nix:386-398 (pins rev = "587a5a1b…" and the four lootboxPatches); ASSESSMENT.md:9-10 “the fork is already 51 commits ahead”

CODE-02 Real runtime chosen per run: deno, then bun, then node

  • What. mcpx runs a script on a real JavaScript runtime: the one requested, otherwise the first of deno, bun or node on PATH. lootbox always uses Deno; opencode uses its own interpreter (CODE-03); Cloudflare uses a V8 isolate (CODE-04).
  • Where. mcpx only.
  • mcpx @ 05c78b2. Detect (internal/runner/runner.go:72-106); setting script.runtime, enum auto|deno|bun|node (internal/settings/registry.go:176-178).
  • Value to mcpx. + med: scripts run wherever any one of three runtimes is installed. − the three behave differently: only Deno has permission flags (internal/runner/runner.go:43-45), and node reaches the daemon over TCP only (internal/codegen/emit.go:289-291, CODE-50).
  • Effort. S — built.
  • Risk. A script that works under Deno can fail under node’s strip-only TypeScript, which rejects enums, namespaces and parameter properties. A cross-runtime test already caught the generated client failing on node for exactly that reason (ASSESSMENT.md:260-261).
  • Detail. Deno runs run --quiet --no-check <perms> <file>; bun runs bun run <file>; node runs --no-warnings --experimental-strip-types <file> (internal/runner/runner.go:84-101). On bun and node a shim fills in Deno.args, Deno.env and Deno.readTextFile, and readTextFile refuses unless MCPX_ALLOW_READ=1 (internal/codegen/emit.go:348-387, guard at :373). ASSESSMENT measured a trivial script against a local client file at 30–60 ms on any of the three (ASSESSMENT.md:79-80).
  • Sources. internal/runner/runner.go:73 candidates := []string{"deno", "bun", "node"}; .lootbox/src/lib/execute_llm_script.ts:38 await new Deno.Command("deno", {

CODE-03 opencode runs a confined interpreter with no ambient APIs

  • What. opencode transpiles the program, parses it with acorn and runs it in its own tree-walking evaluator. The program can reach only the host’s tools tree: no filesystem, network, process, module, import or timer access.
  • Where. opencode v1 and v2 (packages/codemode). lootbox and mcpx run a real runtime; Cloudflare runs a real V8 isolate.
  • mcpx @ 05c78b2. Runs deno/bun/node with --allow-all by default (internal/runner/runner.go:47-54), and AGENTS.md:28-29 promises scripts “the full runtime: filesystem, network, subprocesses”.
  • Value to mcpx. − adopting it contradicts mcpx’s stated choice of full authority (internal/runner/runner.go:47) and breaks every script that touches the filesystem or a subprocess.
  • Effort. XL — packages/codemode/src is 6,878 lines of TypeScript in v1 and 9,456 in v2 (wc -l).
  • Risk. None if not adopted.
  • Detail. v2’s execute description tells the model “Do not use fetch; all external access goes through tools” (v2:packages/core/src/codemode/tool.ts:63). v2 has an opt-in host extension mechanism for classes and functions (v2:packages/codemode/interpreter-support.md:455-456), but opencode core registers none: Extension.make appears only in the package’s own tests and a doc comment (v2:packages/codemode/src/codemode.ts:40). The interpreter yields between steps, so a timeout also stops while (true) {} (v1:packages/codemode/README.md:286), and data crossing a boundary nests at most 32 levels (v1:packages/codemode/README.md:288, v2:packages/codemode/README.md:187). mcpx’s own comparison calls this “a genuinely stronger posture than permissions” (OPENCODE-V2.md:190-193). OPENCODE-V2.md reviewed branch v2@37049a5, not the pinned 2.0.3 (OPENCODE-V2.md:12), and is wrong at the pin in three places here. The doc says packages/core/src/codemode/ contains “fetch” (OPENCODE-V2.md:29) and that fetch “is provided as an extension” (OPENCODE-V2.md:51); the source has only catalog.ts, instructions.ts and tool.ts there, and the tool says not to use fetch. The doc says “10,764 lines of implementation, 20,075 lines of tests across 34 files” (OPENCODE-V2.md:19); the source has 9,456 lines under src/, and test/ holds 43 files, 35 of them .ts totalling 18,056 lines.
  • Sources. v2:packages/codemode/README.md:7 “Rather than trying to sandbox arbitrary JavaScript, CodeMode only runs the language features we implement.”; v1:packages/codemode/src/interpreter/runtime.ts:1 import { parse } from "acorn"; v1:packages/codemode/README.md:5; v2:packages/core/src/codemode/tool.ts:63; OPENCODE-V2.md:51 “is provided as an extension. Timers, imports, filesystem and process”

CODE-04 Cloudflare runs each execution pass in a fresh V8 isolate

  • What. Cloudflare’s DynamicWorkerExecutor loads the generated code into a new Dynamic Worker for every pass, through the Worker Loader binding, and discards it afterwards.
  • Where. Cloudflare only.
  • mcpx @ 05c78b2. One OS process per run, in its own process group (internal/runner/runner.go:413).
  • Value to mcpx. − needs workerd or the Cloudflare platform; mcpx scripts need the local filesystem and processes.
  • Effort. XL — a workerd dependency.
  • Risk. None; not a candidate.
  • Detail. After an approval, a paused execution re-runs the whole program in a new pass, so “Durable state therefore cannot live inside the sandbox” (how-it-works, “Executor”). Dynamic Workers have no build step, so TypeScript must be compiled to JavaScript first (Dynamic Workers “Getting started”). Blog-only claims not confirmed in the docs: an isolate starts “in a handful of milliseconds using only a few megabytes of memory” (2026 blog); the Worker Loader API was in “closed beta” (2025 blog). Current availability and pricing were not checked. The process-model register has the same fact from the process angle (CODE-04).
  • Also recorded from the process model register. Dynamic Workers have no build step, so TypeScript must be compiled first.
  • Sources. internal/runner/runner.go:413; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#executor (“Executor”: “uses a Dynamic Worker Loader to create an isolated Worker for each execution pass”); https://blog.cloudflare.com/code-mode/ (“Dynamic Worker loading: no containers here”); https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#executor “uses a Dynamic Worker Loader to create an isolated Worker for each execution pass”; https://blog.cloudflare.com/code-mode/ “Isolates are so fast that we can just create a new one for every piece of code the agent runs.”

CODE-05 opencode supports only a subset of JavaScript

  • What. opencode’s interpreter implements a chosen subset. v1 has no classes, no generators, no new Promise and no promise chaining; v2 adds generators and new Promise but still has no classes.
  • Where. opencode v1 and v2. lootbox, mcpx and Cloudflare run the full language.
  • mcpx @ 05c78b2. Full TypeScript on a real runtime (CODE-02).
  • Value to mcpx. + low: confirms mcpx scripts can paste in an existing module; nothing to build.
  • Effort. S — no work.
  • Risk. A model used to one harness writes classes and fails under opencode with UnsupportedSyntax.
  • Detail. v1 exposes no eval, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, new Promise, or .then/.catch/.finally (v1:packages/codemode/README.md:254). v2.0.3’s checklist has 211 [x] and 30 [ ] lines (rg -c); gaps include tagged-template calls (v2:packages/codemode/interpreter-support.md:60), user-defined constructor calls (:139), and classes and private fields (:141), while generators (:152) and new Promise (:232) are present. The doc says “251 checked items, 17 gaps” and lists tagged templates as present (OPENCODE-V2.md:104-105); the source at the pin says 211/30 and tagged templates are a gap.
  • Sources. v1:packages/codemode/README.md:254; v2:packages/codemode/interpreter-support.md:60 “- [ ] Tagged-template calls.”; v2:packages/codemode/interpreter-support.md:141; OPENCODE-V2.md:104 “251 checked items, 17 gaps”

CODE-06 Script type-checked against generated types before it runs

  • What. lootbox runs scripts with deno run --no-check=remote. That flag skips remote modules but type-checks local ones, so the script is checked against the generated client’s types and a type error stops the run.
  • Where. lootbox, on by default (both builds, CODE-01). mcpx: opt-in, off by default. opencode strips types without checking (CODE-07). Cloudflare: the model writes JavaScript and nothing checks it.
  • mcpx @ 05c78b2. script.typecheck defaults to off, with on/strict available (internal/settings/registry.go:244-246); Deno runs with --no-check (internal/runner/runner.go:87-90); the check runs through preflight.TypeCheck when enabled (internal/runner/runner.go:328-340).
  • Value to mcpx. + med: a wrong argument name or type is caught before any tool call has a side effect. − adds latency to every run; the default-off choice is written down (internal/runner/runner.go:87-89).
  • Effort. S — flip the default, or say in the agent instructions that nothing checks.
  • Risk. lootbox’s instructions tell agents “the sandbox will reject any scripts that don’t pass”. An agent moving from lootbox to mcpx loses that check without being told.
  • Detail. Verified with deno 2.9.6: --no-check=remote on a file with a type error exits 1 with “Type checking failed”; plain deno run runs it. mcpx’s runner comment says lootbox’s --reload forced “a full type check of the module graph on every single execution” (internal/runner/runner.go:6-7); what is checked is the local script, not the remote client. The check is shallow: lootbox maps nested objects to Record<string, unknown> (.lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:218-220), whereas mcpx renders $ref/allOf/enum/const/ prefixItems (internal/codegen/ts.go:41-61), so mcpx’s check would catch more once turned on. How each loads the generated client per run (lootbox’s HTTP import with --reload) is the process-model register’s PM-25.
  • Sources. .lootbox/src/lib/execute_llm_script.ts:34 "--no-check=remote",; .lootbox/LLM_QUICK_START.md:113 “Type safety is paramount (the sandbox will reject any scripts that don’t pass)”

CODE-07 TypeScript stripped without checking; v2 workerd build skips transpile

  • What. opencode wraps the program in an async function and runs TypeScript’s transpileModule, which reports syntax diagnostics only, before acorn parses the output. v2 chooses the transpiler through a conditional import; its workerd variant is the identity function, so TypeScript-only syntax becomes a parse error there.
  • Where. opencode v1 and v2. lootbox and mcpx use the runtime’s TypeScript support; Cloudflare needs JavaScript.
  • mcpx @ 05c78b2. Full TypeScript through the runtime (CODE-02); no check by default (CODE-06).
  • Value to mcpx. + low: corrects mcpx’s own comparison. Nothing for mcpx to build.
  • Effort. S.
  • Risk. None.
  • Detail. The doc says “TypeScript is rejected rather than stripped” (OPENCODE-V2.md:121); the source transpiles in both versions. The doc’s correction note already says so (OPENCODE-V2.md:3-6). v2’s package.json maps #transpile to workerd and default conditions (v2:packages/codemode/package.json:23-25). A workerd profile suggests opencode runs code mode inside Cloudflare Workers somewhere, probably the web or console app; not traced.
  • Sources. v1:packages/codemode/src/interpreter/runtime.ts:116; v2:packages/codemode/src/interpreter/transpile.node.ts:10; v2:packages/codemode/src/interpreter/transpile.workerd.ts:7 “so codemode programs are passed through”; OPENCODE-V2.md:121 “TypeScript is rejected rather than stripped”

CODE-08 Default script permissions: --allow-net versus --allow-all

  • What. A lootbox script gets network access and nothing else by default: no filesystem, no environment, no subprocess. The network is also its only path to the daemon (ws://localhost:<port>/ws), so it cannot be removed. An mcpx script gets --allow-all.
  • Where. lootbox: fork default ["--allow-net"] plus --allow-import=localhost:<port>; upstream hard-codes the same (CODE-01). mcpx: all. opencode and Cloudflare have no ambient access at all (CODE-03, CODE-09).
  • mcpx @ 05c78b2. Permissions("") gives --allow-all; net gives --allow-net --allow-env (lootbox’s default plus env); strict gives --allow-net=127.0.0.1 --allow-env (internal/runner/runner.go:51-64); none/off mean “no sandbox”, i.e. --allow-all; the default script.permissions is "all" (internal/defaults/defaults.json:34). Only Deno honours any of it (internal/runner/runner.go:43-45).
  • Value to mcpx. − the difference is policy: the user asked for no sandbox (ASSESSMENT.md:168-169, ASSESSMENT.md:290-292). mcpx’s strict is tighter than lootbox’s default (loopback only).
  • Effort. S — nothing to change; state it where agents read.
  • Risk. An agent following lootbox’s instructions assumes a sandbox exists; under mcpx it does not.
  • Detail. lootbox’s --allow-net is unrestricted, so a “sandboxed” lootbox script can send anything a tool returns anywhere. lootbox’s own tool workers (.lootbox/tools/*.ts) always run --allow-all (.lootbox/src/lib/rpc/worker_manager.ts:298-301), and --no-sandbox switches scripts to --allow-all (.lootbox/src/lib/get_config.ts:250-251); the launchd arguments pass no --no-sandbox (nix:configuration.nix:743-749). ASSESSMENT.md:73 quotes lootbox’s command as deno run --allow-all --reload=… --no-check=remote; the source says --allow-net in both the fork and upstream, so that line is wrong. What a script can read of the daemon’s environment, and the missing Origin check on the endpoints that run scripts, are the auth register’s AUTH-25 and AUTH-24.
  • Sources. .lootbox/src/lib/constants.ts:78 export const DEFAULT_PERMISSION_FLAGS: readonly string[] = ["--allow-net"];; .lootbox/src/lib/get_config.ts:485; internal/runner/runner.go:47 “The default is wide open.”; ASSESSMENT.md:73 “deno run –allow-all –reload=http://localhost:PORT/client.ts –no-check=remote ”

CODE-09 Cloudflare blocks or proxies all outbound network

  • What. DynamicWorkerExecutor sets globalOutbound: null by default, so fetch() and connect() throw. A host may pass a Fetcher instead, which receives every outbound request.
  • Where. Cloudflare. opencode has no network API (CODE-03). lootbox allows any network. mcpx allows everything; strict narrows to 127.0.0.1 under Deno only.
  • mcpx @ 05c78b2. No egress proxy. strict relies on Deno’s --allow-net=127.0.0.1 (internal/runner/runner.go:61-63), which bun and node ignore (internal/runner/runner.go:43-45).
  • Value to mcpx. − mcpx scripts are meant to have the user’s full authority.
  • Effort. L — an HTTP proxy plus per-runtime wiring.
  • Risk. None; not a candidate.
  • Detail. Cloudflare’s argument: “Limiting access via bindings is much cleaner than doing it via, say, network-level filtering or HTTP proxies” (2025 blog, “Isolated by default, but connected with bindings”). The MCP client guide asks for the same isolation (CODE-10).
  • Sources. internal/runner/runner.go:61-63; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#dynamicworkerexecutor (“globalOutbound … null blocks access. A Fetcher receives all outbound requests.”); https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#executor (“External fetch() and connect() calls are blocked by default.”)

CODE-10 MCP client guide: programmatic tool calling requires a sandbox

  • What. The MCP documentation’s client best-practices guide describes code mode as “programmatic tool calling” and says it “requires clients to implement a sandbox environment”: no direct network access, credentials held by the host, timeouts and memory limits, truncated output, and per-call authorization. mcpx has no sandbox by design.
  • Where. The guide is documentation, not specification. The same section is in the docs folder of every revision (2024-11-05 through 2026-07-28, and draft); the 2026-07-28 copy is cited. lootbox (Deno, network only), opencode (interpreter) and Cloudflare (isolate) each run a sandbox of some kind. mcpx does not.
  • mcpx @ 05c78b2. No sandbox, chosen by the user: “I dropped the sandbox, as you asked” (ASSESSMENT.md:168) and “No sandbox. You said you did not want one” (ASSESSMENT.md:290); the runner’s default is --allow-all (internal/runner/runner.go:47-54).
  • Value to mcpx. − adopting the guide’s sandbox contradicts the goal. + low: mcpx’s docs should say plainly that it departs from the guide, and why (local-first, the user’s own authority, stateful servers).
  • Effort. L — a real sandbox means a runtime choice (CODE-03, CODE-04) or an egress proxy (CODE-09).
  • Risk. Readers who take “the MCP spec now blesses the pattern” at face value (ASSESSMENT.md:132-134, ASSESSMENT.md:303-304) miss that the guide’s blessing comes with a sandbox mcpx does not have.
  • Detail. The guide’s integration pattern intercepts calls “over an in-process or stdio channel (so network permissions can stay fully denied)” (:260). lootbox and mcpx both reach their broker over the network or a socket (CODE-49, CODE-50), which is why lootbox cannot drop --allow-net (CODE-08). The guide’s other points map to rows here: typed functions from outputSchema (:181, CODE-44), isError as a thrown exception (:301; content-types register, CT-19), resource limits (:294, CODE-11, CODE-14), output truncation (:295, CODE-16), per-call authorization (:290; plugin-API register, PLG-36: opencode checks every child call, mcpx sees one opaque call), and credentials held by the host (:293; auth register, AUTH-25). Cloudflare argues the same point against shell-based tools by name: the agent “needs a shell … a much broader attack surface than a sandboxed isolate” (2026 blog, “Comparing approaches to context reduction”).
  • Sources. docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:169-170 “requires clients to implement a sandbox environment.”; docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:292 “The sandbox should have no direct network access.”; https://modelcontextprotocol.io/docs/2026-07-28/develop/clients/client-best-practices; ASSESSMENT.md:168 “I dropped the sandbox, as you asked.”

CODE-11 Default whole-script wall-clock timeout

  • What. Defaults: lootbox 10 s, mcpx 120 s, opencode none, Cloudflare 60 s by its API reference and 30 s by its changelog and examples.
  • Where. All five differ. opencode’s timeoutMs has no default and neither host sets one.
  • mcpx @ 05c78b2. exec.timeout is 120s (internal/settings/exec.go:16). On timeout the process group gets SIGTERM, then SIGKILL after a grace period, and the exit code is 124 (internal/runner/runner.go:424-438).
  • Value to mcpx. + low: 120 s suits a browser-driving script; lootbox’s 10 s killed them.
  • Effort. S.
  • Risk. An opencode execute can run until the user cancels; the plugin’s mcpx_exec ignores that cancel (plugin-API register, PLG-38).
  • Detail. lootbox’s CLI client clears its own timeout on any message, including the server’s welcome frame, so client_timeout never fires and a stuck daemon hangs lootbox exec indefinitely (.lootbox/src/lib/lootbox-cli/exec.ts:43-44; welcome sent at .lootbox/src/lib/rpc/managers/connection_manager.ts:163); upstream 587a5a1 has the same clearTimeout in onmessage. opencode v1’s host passes no limits (v1:packages/opencode/src/tool/code-mode.ts:239-260) and v2’s calls CodeMode.make({ tools, ...hooks }) (v2:packages/core/src/codemode/tool.ts:222); v1’s README calls that deliberate for a host “that can interrupt the execution fiber (as OpenCode does on user cancel)” (v1:packages/codemode/README.md:268). In v2 a result returned before cleanup times out stays successful with a TimeoutExceeded warning (v2:packages/codemode/README.md:184-187). Cloudflare’s conflict is unresolved; the API reference is the newest page (2026-07-22). The guide asks for “timeouts and memory limits” (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:294).
  • Sources. .lootbox/src/lib/constants.ts:17 export const DEFAULT_TIMEOUT_MS = 10_000;; v2:packages/codemode/README.md:182 “Execution limits have no default values.”; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#dynamicworkerexecutor (“timeout | number | No | 60000”); https://developers.cloudflare.com/changelog/post/2026-02-20-codemode-sdk-rewrite/ (“default 30s”)

CODE-12 Per-tool-call timeout inside a script

  • What. Each tool call a script makes has its own deadline, separate from the script’s: lootbox 30 s, mcpx 120 s, opencode v1 the SDK’s 60 s (reset by progress), opencode v2 12 hours.
  • Where. All four local products differ; Cloudflare documents only the executor timeout.
  • mcpx @ 05c78b2. pool.callTimeout is "120s" (internal/defaults/defaults.json:6), the same length as exec.timeout, so one slow call can use the whole script budget.
  • Value to mcpx. + low: informational; the split exists.
  • Effort. S.
  • Risk. A call that legitimately runs past 120 s dies (see CODE-13 for the fix other clients use).
  • Detail. lootbox wraps client.callTool(...) in a Promise.race against rpcTimeout (default 30 s, .lootbox/src/lib/constants.ts:20), which abandons the promise without cancelling it (CODE-18). The generated client’s per-call timeout is config.timeout at startup but client_timeout after a tool file changes (.lootbox/src/lib/rpc/websocket_server.ts:91 versus :151). opencode v1 passes the per-server timeout or experimental.mcp_timeout, else nothing, leaving the SDK’s 60,000 ms, with resetTimeoutOnProgress: true and no maxTotalTimeout (v1:packages/opencode/src/mcp/index.ts:672); code-mode child calls pass timeout: input.entry.tool.timeout (v1:packages/opencode/src/tool/code-mode.ts:154-158). v1’s config text says “Defaults to 5000” while the code uses 30,000 for connect and list (v1:packages/core/src/v1/config/mcp.ts:21, v1:packages/opencode/src/mcp/index.ts:38). opencode v2 splits timeout.startup/catalog/execution, defaulting to 30 s, 30 s and 12 h (v2:packages/core/src/mcp/client.ts:34, :241).
  • Sources. internal/defaults/defaults.json:6; .lootbox/src/lib/rpc/execute_mcp.ts:60-64; v2:packages/core/src/mcp/client.ts:34 “const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours”

CODE-13 Progress token requested so long calls outlive the timeout

  • What. opencode passes a no-op onprogress to every callTool, which makes the SDK send _meta.progressToken (equal to the JSON-RPC id). v1 also sets resetTimeoutOnProgress, so a server that keeps reporting progress keeps the call alive. mcpx never sends a progress token, so upstream servers cannot report progress to it and its 120 s call timeout cannot be extended.
  • Where. opencode v1 (token and reset) and v2 (token, no reset). lootbox: no token, and its Promise.race is not reset. Cloudflare: not documented.
  • mcpx @ 05c78b2. progressToken appears only in the Progress struct it parses (internal/mcpclient/client.go:438).
  • Value to mcpx. + med: long browser or crawl tools that report progress would not die at 120 s, and progress could flow into /v1/exec stream frames (CODE-26) and plugin metadata (plugin-API register, PLG-37). Today the stream’s progress plumbing can never carry upstream progress for a tool call.
  • Effort. S — add _meta.progressToken to tools/call, reset the deadline on each progress notification, up to a hard maximum.
  • Risk. A server that sends progress forever never times out, so a hard wall is needed; v1 sets no maxTotalTimeout.
  • Detail. v2’s comment says “Requesting progress keeps long calls alive under the SDK’s timeout; execution is the hard wall”, but v2 never sets resetTimeoutOnProgress (rg finds none in v2:packages/core/src), and the SDK resets only when it is set; the 12-hour execution value is the SDK timeout itself. The 1.x SDK reads resetTimeoutOnProgress ?? false (@modelcontextprotocol/sdk@1.22.0/dist/esm/shared/protocol.js:306-308). v1’s comment: “The MCP SDK only sends a progress token when this hook is present, enabling timeout resets”.
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:154-158; v1:packages/opencode/src/mcp/catalog.ts:61 resetTimeoutOnProgress: true,; v2:packages/core/src/mcp/client.ts:280-281; internal/mcpclient/client.go:438; internal/defaults/defaults.json:6

CODE-14 CPU-time and subrequest caps per invocation

  • What. A Cloudflare Dynamic Worker accepts limits: { cpuMs, subRequests }, in its code or at getEntrypoint(), and the lower value wins. Hitting either throws at once.
  • Where. Cloudflare. No local product caps CPU. opencode’s interpreter yields between steps, so its timeout also stops busy loops (v1:packages/codemode/README.md:286).
  • mcpx @ 05c78b2. Wall clock only (CODE-11).
  • Value to mcpx. − scripts run on the user’s own machine with full authority; a CPU cap adds little beyond the timeout.
  • Effort. M — setrlimit per runtime.
  • Risk. None; not a candidate.
  • Detail. Without custom limits the Workers plan limits apply. The guide asks for “memory limits” as well as timeouts (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:294); no local product sets one.
  • Sources. v1:packages/codemode/README.md:286; https://developers.cloudflare.com/dynamic-workers/usage/limits/#set-custom-limits (limits: { cpuMs: 10, subRequests: 5 })

CODE-15 Tool-call budget per execution

  • What. opencode’s library has a maxToolCalls knob; going over it gives a ToolCallLimitExceeded diagnostic. In v2, search calls count too. opencode’s own host never sets it.
  • Where. opencode v1 and v2 (library only). lootbox, mcpx and Cloudflare have no per-run budget.
  • mcpx @ 05c78b2. None; a looping script is stopped by the 120 s timeout or by pool contention.
  • Value to mcpx. + low: bounds fan-out cost from mcpx_exec; the timeout already bounds it roughly.
  • Effort. S — count calls per MCPX_RUN in /v1/call.
  • Risk. Low. From the other side: an opencode code-mode script calling mcpx over MCP is itself unbounded, so mcpx should expect long many-call bursts.
  • Detail. v1 README: “none - unlimited” (v1:packages/codemode/README.md:265); v2 README: “Search counts toward maxToolCalls” (v2:packages/codemode/README.md:171-172).
  • Sources. v1:packages/codemode/README.md:265; v2:packages/codemode/README.md:179; v2:packages/core/src/codemode/tool.ts:222

CODE-16 Cap on the size of returned output

  • What. opencode’s maxOutputBytes truncates the value and logs with an in-band marker and sets truncated: true. Cloudflare ships truncateResult/truncateResponse with a 6,000-token default, and openApiMcpServer clips text at about 6,000 tokens. mcpx returns all of stdout.
  • Where. opencode library (the host leaves it unset and relies on its generic tool-output truncation). Cloudflare utilities. lootbox and mcpx return stdout whole.
  • mcpx @ 05c78b2. stdout accumulates in an unbounded strings.Builder and is returned whole (internal/execsvc/execsvc.go:413-417, :510). Only stderr is tail-bounded (defaults.ExecStderrLimit, internal/execsvc/execsvc.go:436), and only inline artifacts are capped (artifacts.inlineMaxBytes, docs/exec.md:323).
  • Value to mcpx. + high: one console.log(bigObject) in mcpx_exec, over MCP or through the plugin (which prints the whole envelope; plugin-API register, PLG-40), floods the caller’s context, which is exactly what code mode exists to prevent.
  • Effort. S — an exec.maxOutputBytes setting that truncates with a marker and sets truncated.
  • As built (#1937, 2026-10-31). Landed as specified: setting exec.maxOutputBytes (KindBytes, default 64KiB, defaults.json exec.stdoutLimit following the stderrLimit pattern), wired into execsvc.Limits.StdoutLimit by both the daemon (/v1/exec) and the CLI local path. The cap is applied once, at the end of execsvc.Service.Run where the Result is handed back, so every returning surface sees the same capped Stdout: the head is kept (UTF-8-boundary safe), the returned string never exceeds the cap, it ends with a marker naming kept/total/dropped bytes, and Result.StdoutTruncated (stdoutTruncated) is set. Internal consumers (the JSON answer parse, hooks) still see the full text, and the full output is unchanged wherever a full record already exists. 0 disables the cap. Tests: internal/execsvc/stdout_limit_test.go (over-cap capped+marker, under-cap and exact-cap byte-identical, 0 disables, UTF-8 boundary).
  • Risk. Without it, one chatty script can cost more tokens than the direct call it replaced.
  • Detail. v1’s README tells hosts without their own truncation to set it, “or oversized results silently flood model context” (v1:packages/codemode/README.md:268). v2 gives warnings a separate budget equal to maxOutputBytes (v2:packages/codemode/README.md:184). Cloudflare’s durable runtime also limits stored values to 1,000,000 characters (how-it-works, “Durable value and result limits”). The guide: “Validate and truncate sandbox console output before feeding it back to the model” (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:295).
  • Sources. v1:packages/codemode/README.md:284 “Exceeding a configured maxOutputBytes never fails the execution.”; internal/execsvc/execsvc.go:413-417; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#code-and-output-utilities (“The default budget is 6000 estimated tokens”); https://developers.cloudflare.com/agents/tools/codemode/api-reference/#openapimcpserver (“Text responses are limited to approximately 6,000 tokens”)

CODE-17 Concurrent tool calls within one program

  • What. opencode v1 runs at most 8 eagerly started tool calls at once, through a semaphore. v2 removed the cap and tells the model to use Promise.all.
  • Where. opencode v1 (8), v2 (unrestricted). Cloudflare advises sequential calls whenever a run might pause (CODE-37). lootbox and mcpx have no per-script cap.
  • mcpx @ 05c78b2. None per script. The pool caps instances per server at 4 (internal/defaults/defaults.json:3); Promise.all over 50 calls to a shared server multiplexes on one process.
  • Value to mcpx. + low: a per-run cap would protect upstream servers from a single fan-out.
  • Effort. S.
  • Risk. Low. The other direction matters more: an opencode v2 script sends concurrent tools/call to mcpx on one connection, and mcpx’s stdio server handles requests in line (transports register), so they serialise.
  • Detail. v1: “At most 8 tool calls run concurrently” and “Neither is part of the public contract” (v1:packages/codemode/README.md:249, :288); the constant feeds Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) (v1:packages/codemode/src/interpreter/runtime.ts:628).
  • Sources. v1:packages/codemode/src/stdlib/promise.ts:6 export const TOOL_CALL_CONCURRENCY = 8; v2:packages/codemode/README.md:187 “Tool-call concurrency is unrestricted.”; v2:packages/core/src/codemode/tool.ts:67

CODE-18 Upstream call cancelled when the script times out or dies

  • What. When a script times out or dies, mcpx cancels its in-flight upstream request with notifications/cancelled. lootbox’s per-call timeout abandons the promise, and a killed script’s calls keep running in the daemon.
  • Where. mcpx (request context, then notifications/cancelled). opencode (signal on callTool, or fiber interruption; v2 on modern HTTP aborts the request instead). lootbox: neither. Cloudflare: unverified.
  • mcpx @ 05c78b2. /v1/call passes r.Context() to reg.Call (internal/daemon/server.go:696); when that context ends, the client sends notifications/cancelled (internal/mcpclient/client.go:709-716).
  • Value to mcpx. + low: already works.
  • Effort. S.
  • Risk. n/a.
  • Detail. mcpx’s reason is always "timeout", even when the cause is a disconnect or a kill (internal/mcpclient/client.go:714). lootbox passes no signal (.lootbox/src/lib/rpc/execute_mcp.ts:61); the SDK’s own 60 s default (@modelcontextprotocol/sdk@1.22.0/dist/esm/shared/protocol.js:5) eventually sends notifications/cancelled itself, 30 s after lootbox’s rpcTimeout gave up.
  • Sources. .lootbox/src/lib/rpc/execute_mcp.ts:199-208; internal/daemon/server.go:696; internal/mcpclient/client.go:709-716

CODE-19 Final top-level expression becomes the result

  • What. In opencode v2 a program without return returns its last top-level expression, REPL-style. Cloudflare’s createCodeTool “auto-returns the last expression”. v1 returns null. lootbox has no return value at all, and mcpx uses stdout.
  • Where. opencode v2 and Cloudflare. v1: null. lootbox: “Do not return from scripts always log”. mcpx: stdout, emit(), or a default export’s return value.
  • mcpx @ 05c78b2. The mcpx exec prelude only binds names (internal/execsvc/execsvc.go:607-625); output is stdout, emit() or a default export (AGENTS.md:53, AGENTS.md:69-71).
  • Value to mcpx. + med: models naturally end with await ns.tool({...}) and expect a result; REPL semantics save a turn when they forget console.log.
  • Effort. M — last-expression detection and rewrite for exec snippets only, not named scripts.
  • Risk. A snippet ending in an expression with side effects prints where it did not before.
  • Detail. v1: “A program that returns undefined, including by reaching the end without return, produces null” (v1:packages/codemode/README.md:65); v2 also maps undefined to null. Cloudflare’s 2025 blog said results come back “by invoking console.log()”; the current SDK returns the function’s value as result with logs beside it (CodeOutput = { result: unknown; logs?: string[] }, API reference), so the blog is historical. The guide says the model sees “typically the output of console.log statements or a final return value” (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:284).
  • Sources. v2:packages/codemode/README.md:20 “Without an explicit return, the final top-level expression becomes the result.”; .lootbox/LLM_QUICK_START.md:93; https://developers.cloudflare.com/dynamic-workers/examples/codemode/ (“createCodeTool”: “auto-returns the last expression”)

CODE-20 What happens to un-awaited calls when the program ends

  • What. When the program finishes, opencode v1 awaits un-awaited tool calls and reports a failure among them as an unhandled rejection; v2 interrupts them and returns unhandled rejections as warnings.
  • Where. opencode v1 awaits; v2 interrupts. In lootbox and mcpx a pending fetch keeps the runtime’s event loop alive, so the calls complete (runtime semantics, not code). Cloudflare: unverified.
  • mcpx @ 05c78b2. No explicit handling; pending calls finish unless the timeout kills the process group (internal/runner/runner.go:424-438).
  • Value to mcpx. + low: informational; a model moving between harnesses meets different semantics.
  • Effort. S.
  • Risk. Under v2, a forgotten await on a write cancels it silently.
  • Detail. v2’s tool description warns the model: “Await every call whose completion matters; pending calls are interrupted when execution ends” (v2:packages/core/src/codemode/tool.ts:67). The flip between versions is one of two (with CODE-17) where instructions written for one version mislead on the other.
  • Sources. v1:packages/codemode/README.md:249 “When a program completes, still-running un-awaited calls are awaited”; v2:packages/codemode/README.md:18-19 “anything still running is interrupted.”

CODE-21 Working directory the script runs in

  • What. The lootbox CLI sends the script’s text over the WebSocket; the daemon writes it to a temp file and spawns deno with no cwd, so relative paths resolve against the daemon’s directory (launchd sets ~/.config/nix), not the caller’s.
  • Where. lootbox always runs daemon-side. mcpx runs locally with the caller’s cwd by default; /v1/exec and --remote pass the caller’s cwd. opencode and Cloudflare have no working directory.
  • mcpx @ 05c78b2. The runner inherits the caller’s directory (internal/runner/runner.go:354-358); /v1/exec takes cwd/env/stdin as the caller’s context (docs/exec.md:102-105).
  • Value to mcpx. + low: already solved (but the plugin’s mcpx_exec never sends cwd; plugin-API register, PLG-39).
  • Effort. S — done.
  • Risk. A lootbox script that writes ./out.json writes into ~/.config/nix.
  • Detail. lootbox sends stdin as a JS constant, const $STDIN = <json>, prepended to the source (.lootbox/src/lib/lootbox-cli/exec.ts:155-168). With no --allow-read by default (CODE-08), the cwd mostly matters for writes, which also need a permission.
  • Sources. .lootbox/src/lib/lootbox-cli/exec.ts:40 ws.send(JSON.stringify({ script, id }));; .lootbox/src/lib/execute_llm_script.ts:20; nix:configuration.nix:742 WorkingDirectory = "/Users/drewry.pope/.config/nix";

CODE-22 Caller chooses local or remote execution

  • What. exec.where is auto, local or remote. auto runs locally when the daemon is local. A remote run sends the script text, not its path. --local against a remote daemon is refused.
  • Where. mcpx only. lootbox is always daemon-side (CODE-21); opencode always in-process; Cloudflare always on the platform.
  • mcpx @ 05c78b2. docs/exec.md:54-73; the CLI resolves where to run at internal/cli/commands.go:520-530.
  • Value to mcpx. + med: a remote daemon is usable without every call crossing the network.
  • Effort. S — done.
  • Risk. --export and relative imports do not survive a remote run; both are refused (docs/exec.md:69-73).
  • Detail. Remoteness is declared by the caller, never inferred from the transport (docs/exec.md:189-192).
  • Sources. docs/exec.md:54 “exec.where is auto, local or remote”

CODE-23 How arguments reach a script

  • What. A lootbox script receives input only on stdin, through an injected stdin() helper with .json(), .text(), .lines() and .raw(). mcpx passes Deno.args, calls a default export with argv, calls a named export with --export, and fills recipe @param placeholders. A Cloudflare snippet takes one input value.
  • Where. lootbox stdin; mcpx argv, exports, placeholders; Cloudflare codemode.run(name, input?); opencode none.
  • mcpx @ 05c78b2. AGENTS.md:25, AGENTS.md:69-71; launcher at internal/runner/runner.go:530-579; recipes at internal/recipes/recipes.go:12-17.
  • Value to mcpx. + low: already covered.
  • Effort. S.
  • Risk. n/a.
  • Detail. lootbox injects stdin only when stdin is not a TTY (.lootbox/src/lib/lootbox-cli/exec.ts:156). Combined with the type check (CODE-06), a script that calls stdin() run from a terminal would fail with “Cannot find name ‘stdin’” (inferred, not run).
  • Sources. .lootbox/src/lib/lootbox-cli/exec.ts:158-167; .lootbox/README.md:219-226; AGENTS.md:25

CODE-24 Where named scripts are looked up

  • What. lootbox tries the path as given, then <scripts_dir>/<file> in one directory. mcpx walks up from the working directory looking for .mcpx/scripts/, then checks ~/.config/mcpx/scripts/; the nearest wins.
  • Where. lootbox (one directory, shared across repos in practice through lootbox-link symlinks); mcpx (search path); Cloudflare (named snippets in durable storage, CODE-39); opencode (none).
  • mcpx @ 05c78b2. AGENTS.md:21-22; builtinScriptDirs at internal/cli/scripts.go:69-77.
  • Value to mcpx. + low: per-repo scripts already work.
  • Effort. S.
  • Risk. n/a.
  • Detail. lootbox lists scripts by parsing the first JSDoc block for a description and @example lines (.lootbox/src/lib/lootbox-cli/scripts.ts:14-51).
  • Sources. .lootbox/src/lib/lootbox-cli/exec.ts:103-120; nix:pkgs/lootbox-link/lootbox-link.sh:5; AGENTS.md:21 “A named script lives at”

CODE-25 Named injection points in the script launcher

  • What. mcpx wraps each script in a generated launcher with named injection points: before, prefix, onSuccess, onError, suffix. The launcher can be replaced by a template or removed (none).
  • Where. mcpx only. lootbox prepends an import line (.lootbox/src/lib/execute_llm_script.ts:18); opencode and Cloudflare wrap the code in an async function.
  • mcpx @ 05c78b2. internal/runner/runner.go:190-205 (phases); launcher none at :497-502.
  • Value to mcpx. + low: an operator can add tracing or cleanup around every script without editing scripts.
  • Effort. S — done.
  • Risk. None.
  • Detail. The launcher imports the module dynamically so that injected lines really run first (internal/runner/runner.go:523-525). docs/exec.md:39-43 explains why these knobs stay CLI-only rather than joining the /v1/exec wire type.
  • Sources. internal/runner/runner.go:162-165; internal/runner/runner.go:190-205

CODE-26 emit() streams values out while the script runs

  • What. emit(value) writes each value as it is produced. /v1/exec can stream ordered frames: start, log/emit/stdout, artifact metadata, result or error, end, then artifact bodies. The format is SSE, or NDJSON on request.
  • Where. mcpx only. lootbox buffers everything with .output() until the process exits. opencode streams only tool-call status to its UI (plugin-API register, PLG-37), never program output. Cloudflare returns {result, logs} at the end.
  • mcpx @ 05c78b2. AGENTS.md:62-67; docs/exec.md:137-148; emitResult at internal/codegen/emit.go:847.
  • Value to mcpx. + med: long browser scripts can report progress, and a consumer can hang up before paying for artifact bodies.
  • Effort. S — done.
  • Risk. None.
  • Detail. A disconnecting stream consumer cancels the run context and the process group is killed (docs/exec.md:156-160). The plugin does not consume the stream (PLG-37).
  • Sources. .lootbox/src/lib/execute_llm_script.ts:38-43; docs/exec.md:129 “stream — frames as they happen.”

CODE-27 Structured log records on a side channel

  • What. log.info("msg {x}", {x}) writes a record to stderr behind a sentinel (\x1emcpx\x1e). The runner separates records from plain stderr and collects them into logs[] with level and attributes; call sites are captured for warn and above.
  • Where. mcpx only. lootbox gives raw stderr (and drops it on success, CODE-28). opencode and Cloudflare give logs: string[] of console output.
  • mcpx @ 05c78b2. Sentinel at internal/codegen/emit.go:556; parsing at internal/runner/runner.go:374-398.
  • Value to mcpx. + low: a machine-readable account of why a script failed, queryable later through mcpx_log.
  • Effort. S — done.
  • Risk. None.
  • Detail. captureConsole mirrors console.* into records and is on by default (internal/defaults/defaults.json:35).
  • Sources. internal/codegen/emit.go:556 const LOG_SENTINEL = "\u001emcpx\u001e";; AGENTS.md:56-57

CODE-28 lootbox drops stderr on success and stdout on failure

  • What. lootbox’s executor returns {output, warnings: stderr} on success and {error: stderr, output: stdout} on failure, but the message router forwards only output on success and only error on failure. console.error from a successful run, and partial stdout from a failed one, never reach the caller.
  • Where. lootbox. mcpx keeps both. opencode appends logs to the output on both paths. Cloudflare returns logs in both its completed and error variants.
  • mcpx @ 05c78b2. stderr is kept so a non-zero exit is explained (internal/execsvc/execsvc.go:430-438, :518-525).
  • Value to mcpx. + low: already correct.
  • Effort. S — done.
  • Risk. n/a.
  • Detail. lootbox’s history DB stores both fields (.lootbox/src/lib/execute_llm_script.ts:59-67, :77-84), so the data exists; only the WebSocket reply drops it.
  • Sources. .lootbox/src/lib/rpc/managers/message_router.ts:111-115; .lootbox/src/lib/execute_llm_script.ts:86-90

CODE-29 One JSON document per run

  • What. mcpx returns {runId, result, emits[], logs[], stdout, artifacts[], exitCode, durationMs, error?} from --json run, /v1/exec and mcpx_exec. lootbox’s CLI prints the stdout text or the error text and exits 0 or 1.
  • Where. mcpx; opencode (CodeMode.Result internally, flattened to text for the model); Cloudflare (ProxyToolOutput with status completed, paused or error). lootbox has nothing comparable.
  • mcpx @ 05c78b2. internal/execsvc/execsvc.go:201-213; AGENTS.md:73-74.
  • Value to mcpx. + low: already there.
  • Effort. S.
  • Risk. n/a.
  • Detail. When stdout parses as JSON it is also offered parsed as result (internal/execsvc/execsvc.go:527-535), a rule inherited from mcpx --json run (docs/exec.md:134-135). opencode’s Success carries value, logs, truncated and toolCalls (v1:packages/codemode/README.md:128-135).
  • Sources. internal/execsvc/execsvc.go:201-213; .lootbox/src/lib/lootbox-cli/exec.ts:82-93; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#runtime-tool-input-and-output (| { status: "paused"; executionId: string; pending: PendingAction[]; })

CODE-30 Failures reported as typed kinds

  • What. opencode returns failures as data with a kind: ParseError, UnsupportedSyntax, UnknownTool, InvalidToolInput, InvalidToolOutput, InvalidDataValue, ToolCallLimitExceeded, TimeoutExceeded, ToolFailure, ExecutionFailure, and Truncated in v2. Cloudflare’s runtime uses status: "error" and never throws through the tool call.
  • Where. opencode v1/v2 and Cloudflare. mcpx reports exitCode, timedOut and an error string; lootbox an error string and exit 1.
  • mcpx @ 05c78b2. Result.Error string, TimedOut bool, ExitCode int (internal/execsvc/execsvc.go:201-213); nothing separates “script threw”, “tool failed”, “argument invalid”, “did not start” and “timed out”.
  • Value to mcpx. + med: an agent could retry on a tool failure and fix its code on a parse error. The information exists today and is lost in a string.
  • Effort. M — a kind enum, propagated from the runner’s failure paths plus a ToolError marker.
  • Risk. Low.
  • Detail. v2 adds non-fatal warnings beside success (v2:packages/codemode/README.md:127). Cloudflare: “Custom executors should report failures in ExecuteResult.error instead of throwing” (API reference, “Executor”). The guide asks that an uncaught error be surfaced “as the script’s result so the model can self-correct” (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:302-303).
  • Sources. v2:packages/codemode/README.md:145-159; v1:packages/codemode/README.md:292-305; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#runtime-tool-input-and-output (“Sandbox and replay errors use the error output variant.”)

CODE-31 Budgeted catalogue: every namespace, signatures to about 2,000 tokens

  • What. Every namespace is listed with its tool count; full signatures are then added one namespace at a time, cheapest first, until an estimated budget of 2,000 tokens (4 characters per token) runs out. The catalogue says when it is partial.
  • Where. mcpx (mcpx catalog, mcpx_catalog), opencode v1 and v2, all defaulting to 2,000. lootbox lists namespace names and counts only. Cloudflare either puts every type in the tool description (createCodeTool, codeMcpServer) or lists only connector names (durable runtime).
  • mcpx @ 05c78b2. Catalog (internal/codegen/catalog.go:53-68); budget in internal/defaults/defaults.json:30-32; charsPerToken = 4 (internal/codegen/catalog.go:30).
  • Value to mcpx. + low: parity. The difference is delivery: opencode puts the catalogue in front of the model automatically (v1 in the execute tool description, v2 in session instructions); mcpx needs a call, except through the plugin.
  • Effort. S.
  • Risk. None.
  • Detail. The unit is tokens, not characters, in all three. OPENCODE-V2.md:45 says v2 inlines “2,000 characters”; the source estimates tokens as Math.round(text.length / CHARACTERS_PER_TOKEN) with CHARACTERS_PER_TOKEN = 4 (v2:packages/core/src/codemode/catalog.ts:49-50, :187-188), about 8,000 characters. v2 charges namespace header lines against the budget (v2:packages/core/src/codemode/catalog.ts:96-113); v1 counts only full entries (v1:packages/codemode/README.md:184); mcpx charges headers too (internal/codegen/catalog.go:64-66). lootbox’s /rpc-namespaces still tells the model to call a get_namespace_types() function that does not exist (.lootbox/src/lib/rpc/managers/type_generator_manager.ts:258-266). Cloudflare’s durable runtime spends a turn deliberately: the model calls codemode.search()/describe() in one execution and acts in the next (how-it-works, “What the model sees”). lootbox’s discovery re-lists every server on every request and waits for every server at startup (process-model register, PM-23 and PM-22).
  • Sources. v2:packages/core/src/codemode/catalog.ts:50 const INLINE_BUDGET = 2_000; v1:packages/codemode/src/tool-runtime.ts:86 const defaultCatalogBudget = 2_000; internal/codegen/catalog.go:53-68; .lootbox/src/lib/rpc/managers/type_generator_manager.ts:188-200; OPENCODE-V2.md:45 “2,000 characters inline”

CODE-32 Catalogue changes sent as a delta when shorter

  • What. When tools change mid-session, v2 re-renders its instructions as added/changed/removed entries (or namespace count changes when the catalogue is partial), and falls back to the full catalogue when that is shorter.
  • Where. opencode v2. v1’s describeCatalog re-renders everything (v1:packages/opencode/src/tool/registry.ts:283-289). mcpx has the same machinery. lootbox and Cloudflare have none.
  • mcpx @ 05c78b2. Fingerprint, Diff, ShorterOf (internal/codegen/catalog.go:244-357).
  • Value to mcpx. + low: parity; it needs a long-lived consumer, and the plugin is that consumer.
  • Effort. S.
  • Risk. None.
  • Detail. v2 always sends a full replacement when the catalogue flips between partial and complete, or when any namespace description changes (v2:packages/core/src/codemode/instructions.ts:39-50).
  • Sources. v2:packages/core/src/codemode/instructions.ts:34-37; internal/codegen/catalog.go:244-357

CODE-33 Pinned tools always shown in the catalogue

  • What. A v2 tool registered with options.pinned === true is always in the inlined catalogue, and its cost comes off the budget first.
  • Where. opencode v2. mcpx has --bias words to favour matching tools. The others have neither.
  • mcpx @ 05c78b2. Bias scoring only (internal/codegen/catalog.go:223).
  • Value to mcpx. + low: an operator could pin, say, chrome_devtools.take_screenshot.
  • Effort. S — a per-server pinned list in config.
  • Risk. None.
  • Detail. Pinned entries are removed from the round-robin selection order (v2:packages/core/src/codemode/catalog.ts:79-91).
  • Sources. v2:packages/core/src/codemode/catalog.ts:79-84; v2:packages/core/src/codemode/tool.ts:162-166

CODE-34 Tool search callable from inside the program

  • What. A script can search the catalogue without ending the run. mcpx: synchronous search(query, limit = 20) and describe(name). opencode v1: await tools.$codemode.search({query, namespace, limit, offset}), through a reserved namespace. opencode v2: a synchronous search(...) built-in. Cloudflare: await codemode.search(query) (top 50) and codemode.describe(target).
  • Where. mcpx, opencode v1/v2, Cloudflare. lootbox has no search, not even a CLI command.
  • mcpx @ 05c78b2. search at internal/codegen/emit.go:1121, describe at :1167; also mcpx search and the mcpx_search MCP tool (internal/mcpserver/server.go:307).
  • Value to mcpx. + low: parity.
  • Effort. S.
  • Risk. None.
  • Detail. v1 is asynchronous and namespaced, not a synchronous built-in like v2’s: the reserved namespace is "$codemode" and the default limit 10 (v1:packages/codemode/src/tool-runtime.ts:85-87). v1 scores exact path 20, path substring 8, description 4, searchable text 2, with naive singularisation (v1:packages/codemode/README.md:206), and advertises search only when the catalogue is partial but always registers it (v1:packages/codemode/README.md:195). v2 search also matches enclosing namespace descriptions and counts toward maxToolCalls (v2:packages/codemode/README.md:170-172). Cloudflare’s search also returns snippets (CODE-36).
  • Sources. internal/codegen/emit.go:1121 export function search(query: string, limit = 20): ToolMatch[] {; v1:packages/codemode/README.md:198; v2:packages/codemode/README.md:170; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#sandbox-codemode-api (“Results are ranked and limited to 50.”)

CODE-35 In-program search pages its results

  • What. opencode’s search takes offset and returns remaining and next: {offset} | null. An empty query with a namespace lists that namespace; a query naming an exact path is a lookup.
  • Where. opencode v1/v2. mcpx’s search has no offset. Cloudflare returns total and truncated but no cursor.
  • mcpx @ 05c78b2. export function search(query: string, limit = 20) (internal/codegen/emit.go:1121).
  • Value to mcpx. + low: a model can browse a large namespace (chrome’s 27 tools) without guessing words.
  • Effort. S.
  • Risk. None.
  • Detail. v2’s search counts against maxToolCalls (CODE-15). v1 says to “spread it into the original request to preserve its query, namespace, and limit” (v1:packages/codemode/README.md:206).
  • Sources. v1:packages/codemode/README.md:206 “next is { offset } when another page exists and null on the final page”; internal/codegen/emit.go:1121

CODE-36 In-program search also finds saved snippets

  • What. Cloudflare’s codemode.search() ranks connector methods and saved snippets together, and a snippet runs with codemode.run(name, input). mcpx’s in-script search() covers tools only; saved scripts (“recipes”) are matched from the CLI or the daemon, not from inside a script.
  • Where. Cloudflare. mcpx has recipe matching outside scripts (internal/recipes/match.go). lootbox lists scripts with lootbox scripts. opencode has no saved scripts.
  • mcpx @ 05c78b2. search() reads toolMeta only (internal/codegen/emit.go:138-146, :1121).
  • Value to mcpx. + low: an agent mid-script could find “screenshot-and-diff” and reuse it.
  • Effort. M — emit recipe metadata into the client and add run(name, args).
  • Risk. None.
  • Detail. Cloudflare snippets come from host-reviewed executions (CODE-39), so what search returns was vetted; mcpx recipes are whatever is on disk.
  • Sources. internal/recipes/recipes.go:1 “Package recipes turns saved scripts into parameterised, matchable units.”; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#platform-sdk (“codemode.search() searches connector methods and saved snippets.”)

CODE-37 Approval pauses a run by aborting it and replaying it

  • What. A Cloudflare connector method marked requiresApproval records a pending action and aborts the pass. After approval the whole program re-runs with the same execution id; calls already applied return their recorded results, and a replay that diverges fails. codemode.step(name, fn) runs a closure once and records its result so replays return the recorded value.
  • Where. Cloudflare durable runtime only. opencode asks a permission question per call, synchronously, for every child call (plugin-API register, PLG-36). mcpx can ask through elicitation (elicit.confirmDestructive, internal/settings/consumer.go:47). lootbox: none.
  • mcpx @ 05c78b2. No durable pause; a script waiting on an elicitation stays blocked in its own process.
  • Value to mcpx. − mcpx scripts have full local authority and side effects (filesystem, subprocesses) that cannot be replayed deterministically; the design assumes determinism mcpx cannot give.
  • Effort. XL.
  • Risk. None; not a candidate.
  • Detail. Cloudflare warns: “Calls in Promise.all() can arrive in different orders across passes and cause replay divergence” (how-it-works, “Deterministic replay”). Connector calls do not need step(); the runtime records them already. The guide allows “categorical approval” per run but says “the broker must still evaluate each call against that grant” (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:290).
  • Sources. internal/settings/consumer.go:47; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#approvals-through-abort-and-replay (“the runtime records the action as pending and aborts the current pass.”); https://developers.cloudflare.com/agents/tools/codemode/api-reference/#sandbox-codemode-api (“step(name, fn) | Runs a closure once and records its result.”)

CODE-38 Rollback through a per-tool revert

  • What. runtime.rollback() walks the applied connector calls in reverse and calls each tool’s revert(args, result) where one is defined.
  • Where. Cloudflare only.
  • mcpx @ 05c78b2. None.
  • Value to mcpx. − MCP tools have no revert; mcpx would have to invent one per server.
  • Effort. XL.
  • Risk. None; not a candidate.
  • Detail. “Rollback is compensation, not database transaction isolation.” (how-it-works, “Rollback”).
  • Sources. internal/settings/consumer.go:47 (mcpx’s only confirmation mechanism); https://developers.cloudflare.com/agents/tools/codemode/api-reference/#connector-tool-types (revert?: (args: unknown, result: unknown, ctx?: ToolExecuteContext) => Promise<void> | void;)

CODE-39 Only the host may promote a run to a saved snippet

  • What. In Cloudflare the model cannot save its own code: the application reviews an execution and calls runtime.saveSnippet(name, {executionId, description}).
  • Where. Cloudflare. lootbox and mcpx let the agent write files into the scripts directory. opencode has no saved scripts.
  • mcpx @ 05c78b2. Agents write .mcpx/scripts/<name>.ts directly (AGENTS.md:21-22).
  • Value to mcpx. − mcpx’s model is “the agent is the user”; a review gate would get in the way of the main workflow.
  • Effort. M.
  • Risk. An agent can plant a script that a later agent runs through recipe matching (CODE-36).
  • Detail. “The API accepts any execution status, so verify that the execution completed successfully before saving it.” (how-it-works, “Snippets”).
  • Sources. AGENTS.md:21-22; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#snippets (“The model does not promote its own code.”)

CODE-40 Tool names that are JavaScript reserved words get a suffix

  • What. Cloudflare’s sanitizeToolName replaces separators, strips invalid characters, prefixes a leading digit (3d-render becomes _3d_render) and suffixes reserved words (delete becomes delete_). mcpx does the first three but not reserved words.
  • Where. Cloudflare (McpConnector, generateTypesFromJsonSchema). mcpx: invalid characters and leading digit. lootbox: invalid characters only. opencode keeps the original name with bracket notation (CODE-43).
  • mcpx @ 05c78b2. ToolFuncName keeps delete, new or class as is (internal/codegen/ts.go:397-418). toolDecl emits function delete(args…) inside a declaration block (internal/codegen/emit.go:97), which is not valid TypeScript; toolImpl emits method shorthand (internal/codegen/emit.go:217), which is.
  • Value to mcpx. + low: mcpx types prints invalid TypeScript for a tool named delete or import, and AGENTS.md:99-100 calls that output authoritative. The runtime client and --typecheck (which checks against typeof __mcpx.<ns>, internal/codegen/emit.go:1432) are unaffected.
  • Effort. S — a reserved-word table in ToolFuncName.
  • Risk. An agent that pastes mcpx types output into a .d.ts gets a parse error. Low.
  • Detail. AGENTS.md:108-109 documents fancy-name becoming fancy_name and says nothing about reserved words. ASSESSMENT lists a ToolFuncName("9lives") bug already fixed (ASSESSMENT.md:258-259).
  • Sources. internal/codegen/ts.go:413-416; https://developers.cloudflare.com/agents/tools/codemode/mcp/ (step 2: “delete becomes delete_”); https://developers.cloudflare.com/agents/tools/codemode/api-reference/#code-and-output-utilities (“suffixes JavaScript reserved words”)

CODE-41 Namespace names checked against reserved words and prelude names

  • What. Cloudflare validates namespaces: each must be a valid identifier, unique, and must not shadow an executor global. mcpx’s SanitizeNamespace fixes invalid characters and a leading digit but checks neither reserved words (default, import, new, delete) nor the prelude’s own names (tools, log, emit, call, artifact, readResource, ToolError).
  • Where. Cloudflare validates. mcpx does not. lootbox is safe because MCP namespaces always carry an mcp_ prefix. opencode uses bracket notation.
  • mcpx @ 05c78b2. SanitizeNamespace (internal/config/config.go:407-416). The client emits export const <ns> = { per namespace (internal/codegen/emit.go:122); the exec prelude imports tools, { call, readResource, artifact, log, emit, ToolError, installGlobals, captureConsole } and then const { <every ns> } = tools; (internal/execsvc/execsvc.go:610-617).
  • Value to mcpx. + med: a server configured as log or default would, going by the code, make the client or the prelude fail to parse, breaking every mcpx exec and script, not just calls to that server. Not run.
  • Effort. S — reject or suffix reserved and prelude names in SanitizeNamespace and say so in mcpx ls, or have the prelude skip colliding names.
  • Risk. One unlucky server name takes out all scripting, with a parse error that does not name the server.
  • Detail. An explicit namespace in config bypasses the sanitiser: pick(ex.Namespace, SanitizeNamespace(name)) (internal/config/config.go:480); whether that value is validated elsewhere is unchecked.
  • Sources. internal/config/config.go:407-416; internal/execsvc/execsvc.go:610-617; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#dynamicworkerexecutor (“Names must be valid JavaScript identifiers, unique, and must not shadow executor globals.”)

CODE-42 Two tool names that sanitise to the same identifier

  • What. Cloudflare’s McpConnector throws when two source names produce the same identifier, and toolName() can be overridden to tell them apart. mcpx emits duplicate object keys, so the last one wins at run time and a type check fails. lootbox’s lookup uses find, so the first wins, and colliding server names silently overwrite each other.
  • Where. Cloudflare detects it; mcpx and lootbox do not; opencode avoids it by not renaming (CODE-43).
  • mcpx @ 05c78b2. No collision check in Module or toolImpl (internal/codegen/emit.go:108-146, :204-219); rg finds no “collid” or “duplicate” in codegen.
  • Value to mcpx. + med: a-b and a_b on one server currently call whichever sorts last, silently.
  • Effort. S — detect while building the namespace and suffix _2, or warn.
  • Risk. The wrong tool is called with no error.
  • Detail. lootbox sanitises server names at load (.lootbox/src/lib/lootbox-cli/server.ts:10-11, :82-83), so fff-nix and fff_nix in one config collide; its tool lookup is schemas.tools.find((t) => t.name === toolName) (.lootbox/src/lib/rpc/execute_mcp.ts:46). The spec’s SHOULD on aggregators disambiguating names is the tools register’s TOOL-02.
  • Sources. internal/codegen/emit.go:108-146; .lootbox/src/lib/rpc/execute_mcp.ts:46; https://developers.cloudflare.com/agents/tools/codemode/mcp/ (step 2: “If two source names produce the same identifier, the connector throws an error.”)

CODE-43 opencode keeps real tool names and uses bracket notation

  • What. opencode keeps the original tool name in its path and renders non-identifier segments with brackets: tools.context7["resolve-library-id"](…). Only characters outside [a-zA-Z0-9_-] become _, so hyphens survive.
  • Where. opencode v1/v2. mcpx, lootbox and Cloudflare rename to identifiers.
  • mcpx @ 05c78b2. Renames (fancy-name to fancy_name, AGENTS.md:108-109).
  • Value to mcpx. + low: the name the model reads in upstream docs is the name it calls, and nothing can collide. − bracket calls are less natural, and bare-identifier binding (chrome_devtools.x) needs identifier namespaces anyway.
  • Effort. M — keep the rename but also accept tools.ns["orig-name"] aliases.
  • Risk. Low.
  • Detail. v2 tells the model “Do not infer or normalize tool names; preserve bracket notation” (v2:packages/core/src/codemode/tool.ts:65) and rejects any tool part over 64 characters (names must match ^[A-Za-z0-9_-]{1,64}$, v2:packages/core/src/tool.ts:297). v1 derives paths from server_tool keys by the longest matching server prefix (v1:packages/opencode/src/tool/code-mode.ts:40).
  • Sources. v2:packages/codemode/README.md:77-78; v2:packages/core/src/tool/runtime.ts:274; AGENTS.md:108-109

CODE-44 Return types generated from a tool’s outputSchema

  • What. opencode feeds each MCP tool’s outputSchema into its signature, so structured results are typed. mcpx types every result as any; lootbox as the raw envelope.
  • Where. opencode v1 (output: entry.tool.def.outputSchema), v2 (output: tool.outputSchema ?? {}), Cloudflare (JsonSchemaToolDescriptor.outputSchema). lootbox: Promise<McpToolResult>. mcpx: ToolResult = any. outputSchema first appears in 2025-06-18.
  • mcpx @ 05c78b2. mcpclient.Tool parses OutputSchema (internal/mcpclient/client.go:122), but codegen.Tool has no such field (internal/codegen/ts.go:19-23) and every function returns Promise<ToolResult> with export type ToolResult = any; (internal/codegen/emit.go:458).
  • Value to mcpx. + med: mcpx already unwraps structuredContent (content-types register, CT-15), so typing it is nearly free and makes --typecheck meaningful for results as well as arguments. The guide’s step 1 is exactly this (docs/docs/2026-07-28/develop/clients/client-best-practices.mdx:181).
  • Effort. S — thread OutputSchema into codegen.Tool and render Promise<T & {raw}> when present.
  • Risk. A server’s structuredContent may not match its schema; types are advisory. The spec says clients SHOULD validate structured results against it.
  • Detail. With an outputSchema present, v2 also stops JSON-parsing text (content-types register, CT-16), so the declared type and the value agree. Cloudflare’s illustrative types show Output = unknown or [key:string]: any. The guide’s advice when no schema exists: “Use a generic type and move on” (:212), which is what mcpx does today for every tool. The outputSchema field itself, across revisions, is the tools register’s TOOL-13.
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:127 output: entry.tool.def.outputSchema as SandboxTool.JsonSchema | undefined,; v2:packages/core/src/tool/mcp.ts:48; schema/2025-06-18/schema.ts:951 outputSchema?: {; 2025-06-18/server/tools.mdx:315 “Clients SHOULD validate structured results against this schema.”; internal/codegen/emit.go:458

CODE-45 lootbox’s generated types carry no descriptions

  • What. lootbox turns MCP tools into interfaces and method signatures with no JSDoc: tool descriptions are stored but never printed, and property descriptions are dropped. mcpx and opencode render JSDoc for tools and properties; Cloudflare renders doc comments.
  • Where. lootbox lacks them; mcpx, opencode, Cloudflare have them.
  • mcpx @ 05c78b2. Tool JSDoc at internal/codegen/emit.go:81-90; per-property jsdoc(p, inner) at internal/codegen/ts.go:229-231; server Instructions and operator Prelude above the signatures (internal/codegen/ts.go:31-37).
  • Value to mcpx. + low: already done.
  • Effort. S.
  • Risk. n/a.
  • Detail. Verified live: curl 127.0.0.1:9420/types/mcp_codedb returned bare interfaces such as codedb_callpath(args: Codedb_McpCodedb_Codedb_callpathArgs): Promise<Codedb_McpToolResult>; with no comments. generateArgsInterface keeps only name, type and optional (.lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:168-178). opencode v1 adds @default/@format tags (v1:packages/codemode/README.md:214).
  • Sources. .lootbox/src/lib/type_system/client_generator.ts:404-479; .lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:195-242; internal/codegen/emit.go:81-90

CODE-46 lootbox generates one function per MCP resource

  • What. lootbox lists each server’s resources and generates tools.mcp_x.resource_<name>(args) per resource, with arguments from URI-template variables, calling readResource.
  • Where. lootbox. mcpx has one generic readResource export. opencode keeps resources out of code mode. Cloudflare’s code-mode docs do not cover resources.
  • mcpx @ 05c78b2. readResource(uri) (internal/execsvc/execsvc.go:610-611).
  • Value to mcpx. − one function per resource bloats the types for servers with many resources; one reader is simpler.
  • Effort. S — nothing to build.
  • Risk. n/a.
  • Detail. The template branch never runs: lootbox reads uriTemplate off resources/list entries (.lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:110-116), but templates arrive from resources/templates/list, which lootbox never calls (rg finds no listResourceTemplates in .lootbox/src). Every generated resource function therefore takes an empty argument object. The resources register carries the same row (CODE-46).
  • Also recorded from the resources register. lootbox only. mcpx has one generic readResource(uri); opencode keeps resources out of code mode. n/a — readResource(uri) (internal/execsvc/execsvc.go:610-611). lootbox’s template branch never runs: it reads uriTemplate off resources/list entries, but templates come from resources/templates/list, which lootbox never calls, so every generated function takes an empty argument object.
  • Sources. .lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:130; .lootbox/src/lib/rpc/execute_mcp.ts:160-194; internal/execsvc/execsvc.go:610-611; .lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:110-116

CODE-47 lootbox exposes local TypeScript files as namespaces

  • What. Any .lootbox/tools/<ns>.ts that exports 0- or 1-argument functions becomes tools.<ns>.<fn>. Types are extracted with ts-morph, and each file runs in a worker process with --allow-all, restarted when the file changes.
  • Where. lootbox only. mcpx turns non-MCP things into servers through config-declared CLI adapters (internal/adapter/adapter.go:1-12) and OpenAPI (mcpx api), and scripts import other scripts (--export).
  • mcpx @ 05c78b2. No TypeScript-module-as-namespace adapter, by choice: a plain module import “already does this without a protocol in between” (ASSESSMENT.md:296-299).
  • Value to mcpx. − ts-morph on the request path was measured as the main cost and the OOM source (ASSESSMENT.md:62-64). ASSESSMENT still calls it “the one real gap” if the user relies on it.
  • Effort. M — a small adapter that serves a module’s exports as an MCP server.
  • Risk. Low.
  • Detail. The deployed lootbox serves five such namespaces (fs, kv, sqlite, memory, graphql), per a live /namespaces call. ASSESSMENT measured extracting them at 46–209 ms each, on every discovery request (ASSESSMENT.md:38).
  • Sources. .lootbox/README.md:86-99; .lootbox/src/lib/rpc/worker_manager.ts:298-301; ASSESSMENT.md:299 “If you use that feature, it is the one real gap.”

CODE-48 lootbox YAML prompt workflows

  • What. lootbox workflow start|step|status|reset|abort walks an agent through YAML-defined prompt steps: Handlebars templates that can loop between a minimum and maximum count, with state in .lootbox-workflow.json and events logged to SQLite.
  • Where. lootbox only.
  • mcpx @ 05c78b2. None, deliberately: “No workflow engine or script history. Lootbox has both; they are separable concerns” (ASSESSMENT.md:294-295).
  • Value to mcpx. − harness-level prompt sequencing, unrelated to reaching MCP servers.
  • Effort. M.
  • Risk. None.
  • Detail. None beyond the above.
  • Sources. .lootbox/README.md:460-471; .lootbox/src/lib/db.ts:71-80; ASSESSMENT.md:294

CODE-49 lootbox’s script-to-gateway protocol is its own WebSocket JSON

  • What. A lootbox client sends {script, id} or {method: "ns.fn" | "mcp_ns.fn", args, id} over ws://…/ws. The server first sends {type: "welcome", functions, config}, then answers {result | error, id}. It is neither JSON-RPC nor MCP; MCP calls are told apart only by the mcp_ prefix.
  • Where. lootbox. mcpx serves REST (/v1/call, /v1/exec) over a unix socket or loopback TCP, and MCP at /mcp. opencode is in-process. Cloudflare uses Workers RPC (CODE-51).
  • mcpx @ 05c78b2. POST /v1/call (internal/daemon/server.go:468); the generated client posts {server, tool, args} (internal/codegen/emit.go:524-535).
  • Value to mcpx. + low: mcpx already has a documented REST surface with an OpenAPI description.
  • Effort. S.
  • Risk. n/a.
  • Detail. lootbox has a second socket, /worker-ws, for its tool workers. Any local client that sends {type:"identify", workerId} is registered as that worker’s sender with no check, and that namespace’s calls are then routed to it (.lootbox/src/lib/rpc/managers/connection_manager.ts:125-131); inferred, not exploited.
  • Sources. .lootbox/src/lib/rpc/managers/message_router.ts:16-32; .lootbox/src/lib/rpc/managers/connection_manager.ts:86-93; .lootbox/src/lib/rpc/websocket_server.ts:273-291

CODE-50 Script client reaches the daemon over a unix socket

  • What. mcpx’s generated client uses the daemon’s unix socket when MCPX_SOCKET is set: through Deno.createHttpClient({proxy:{transport:"unix"}}) on Deno and fetch(u, {unix}) on Bun. It falls back to loopback TCP, which is what node always uses.
  • Where. mcpx. lootbox uses a TCP WebSocket. opencode is in-process. Cloudflare uses Workers RPC.
  • mcpx @ 05c78b2. internal/codegen/emit.go:268-293.
  • Value to mcpx. + med: the socket’s file mode becomes the access control (defaults.PrivateMode, internal/daemon/server.go:202), and calls are about four times faster (0.17 ms versus 0.65 ms, per the comment).
  • Effort. S — done.
  • Risk. node scripts fall back to TCP, which is unauthenticated and checks no Origin (auth register, AUTH-24).
  • Detail. ASSESSMENT recorded a fix for socket paths over macOS’s 104-byte sun_path limit (ASSESSMENT.md:262-264).
  • Sources. internal/codegen/emit.go:258-261; internal/codegen/emit.go:268-293

CODE-51 Cloudflare connects script and host through Workers RPC

  • What. Cloudflare’s sandbox gets one Proxy per namespace, and each call crosses to the host over Workers RPC (ToolDispatcher / ConnectorBinding.callTool). The host intercepts every call for approval, logging and replay.
  • Where. Cloudflare.
  • mcpx @ 05c78b2. HTTP to the daemon (CODE-50; internal/codegen/emit.go:524-535).
  • Value to mcpx. − Workers-only.
  • Effort. XL.
  • Risk. None; not a candidate.
  • Detail. “The runtime intercepts each call before the connector executes it.” (how-it-works, “Connectors”).
  • Sources. internal/codegen/emit.go:524-535; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#connectors (“Connector calls cross the sandbox boundary through Workers remote procedure calls (RPC).”); https://developers.cloudflare.com/agents/tools/codemode/api-reference/#executor (binding: { callTool(method: string, args: unknown): Promise<unknown>; };)

CODE-52 Every run recorded with its script text and output

  • What. Every lootbox execution is written to a script_runs table: id, timestamp, full script text, success flag, full output, error, duration and session id. The write is fire-and-forget, and nothing in lootbox reads the table back.
  • Where. lootbox. mcpx keeps a durable structured log (mcpx_log) and an artifact index. Cloudflare keeps a durable execution log capped at maxExecutions 50. opencode keeps only the session transcript.
  • mcpx @ 05c78b2. Log records and run ids, not the script source (internal/mcpserver/server.go:339-341).
  • Value to mcpx. + low: storing the source per run would let an agent “re-run what worked yesterday” and would feed recipe candidates. − stored output is where secrets end up.
  • Effort. M — a retention setting and redaction.
  • Risk. Output is stored unredacted.
  • Detail. Observed by the research worker on 2026-09-30: the live table held 1,713 runs from 2026-04-05 to 2026-09-30, of which 1,100 succeeded (a 36% failure rate), and none had a session id, because the CLI never sends one (.lootbox/src/lib/lootbox-cli/exec.ts:40). That is a baseline mcpx’s own success rate can be compared with. The live DB is ~/Library/Application Support/lootbox/lootbox.db (.lootbox/src/lib/db.ts:19-23); the .lootbox/lootbox.db file in the checkout is a stray. ASSESSMENT left history out on purpose (ASSESSMENT.md:294-295). The process-model register has the same row (CODE-52).
  • Also recorded from the process model register. Cloudflare keeps a durable execution log capped at maxExecutions 50. mcpx keeps a structured log and an artifact index but not the script source. opencode keeps only the transcript. mcpx_log (internal/mcpserver/server.go:339-341); run ids without source. The live lootbox table holds 1,713 runs (2026-04-05 to 2026-09-30), 36% failed, and none carries a session id because the CLI never sends one. lootbox.db in the checkout is a stray.
  • Sources. .lootbox/src/lib/script_history.ts:33 “Don’t await - fire and forget”; .lootbox/src/lib/script_history.ts:29-63; .lootbox/src/lib/db.ts:97; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#durable-execution-log (“Retention and stale executions”); internal/mcpserver/server.go:339-341

CODE-53 opencode wraps MCP servers in code mode by default

  • What. In v2 each MCP server config has codemode, default true, and every tool whose options.codemode !== false disappears from the direct tool list into execute. In v1 code mode is gated by OPENCODE_EXPERIMENTAL, which the user’s build sets, so the installed v1 does the same for MCP tools. mcpx’s own /mcp is therefore nested inside opencode’s code mode unless the user opts out.
  • Where. opencode v2: on by default, per server; a built-in plugin opts out only executor servers. opencode v1: experimentalCodeMode is enabledByExperimental("OPENCODE_EXPERIMENTAL_CODE_MODE"), the specific flag when set, else OPENCODE_EXPERIMENTAL; the nix wrapper and home profile set OPENCODE_EXPERIMENTAL=1.
  • mcpx @ 05c78b2. Not listed by the exclusion plugin; nothing in mcpx’s docs tells users to opt out (codemode: false in v2, OPENCODE_EXPERIMENTAL_CODE_MODE=0 in v1). The model would write opencode JS that calls tools.mcpx.mcpx_exec({source: "<TS>"}): code nested in code.
  • Value to mcpx. + high: one docs sentence (or a v2 plugin that sets it through mcp.transform) removes two catalogues, two error surfaces and double cost per call.
  • Effort. S.
  • Risk. Not done: models get confused and every call costs twice. Plans that assume v1 shows MCP tools directly are wrong for this machine.
  • Detail. v2 wraps all tools, not only MCP ones; built-ins opt out individually (v2:packages/core/src/tool/plugin/edit.ts:123, v2:packages/core/src/tool/plugin/grep.ts:77), and the whole of code mode is switched off only by a deny rule on execute. v1 wraps MCP tools only (v1:packages/opencode/src/tool/code-mode.ts:210), registers none of them directly once the flag is on, and still adds three resource tools when a server declares resources. The exclusion plugin matches local executor mcp and remote https://executor.sh/<x>/mcp (v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:7, :16). For remote servers in code mode, v2 also appends ?codemode=false to the URL, asking servers that bundle their own code mode for raw tools, and retries without it on 400 or 404; mcpx neither sends nor honours it (plugin-API register, PLG-35). The plugin-API register holds the same default from the plugin angle (CODE-53, and PLG-27 for plugin tools). OPENCODE-V2.md is wrong at the pin in two places here. The doc calls the exclusion plugin “mcp-codemode-defaults.ts” and says it turns code mode off “for servers that are themselves code mode” (OPENCODE-V2.md:53-54); the source names it mcp-codemode-exclusion.ts and matches only executor. The doc says “If you stay on opencode v1 … then none of v2’s Code Mode is available to you today” (OPENCODE-V2.md:222-225); the source enables it from the environment the user’s build sets, and the doc’s correction note says only that v1 ships it “behind” the flag (OPENCODE-V2.md:4-5).
  • Also recorded from the plugin apis register. opencode v2 (per server, default on); opencode v1 (global, off unless the flag is set; this user’s nix build sets OPENCODE_EXPERIMENTAL=1, so v1 here already runs code mode). Not excluded. mcpx’s /mcp is wrapped: the model writes opencode code-mode JS that calls tools.mcpx.mcpx_exec({source: "<TS>"}), code nested in code. OPENCODE-V2.md:53 calls the file mcp-codemode-defaults.ts and says it turns code mode off “for servers that are themselves code mode”; the file is mcp-codemode-exclusion.ts and matches only executor. v2 also adds “Use tools from this server through execute” to a code-mode server’s instructions (capabilities area).
  • Sources. v2:packages/schema/src/mcp.ts:34-36 “Expose this server’s tools through Code Mode. Defaults to true.”; v2:packages/core/src/tool.ts:228-229; v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:16; v1:packages/opencode/src/effect/runtime-flags.ts:11-14; v1:packages/opencode/src/effect/runtime-flags.ts:48; v1:packages/opencode/src/session/tools.ts:388 if (flags.experimentalCodeMode) return tools; nix:flake.nix:297 “–set-default OPENCODE_EXPERIMENTAL 1”; nix:homeUser.nix:115; OPENCODE-V2.md:53 “that turns opencode’s code mode”; v2:packages/core/src/plugin/mcp-codemode-exclusion.ts:7

CODE-54 Code execution reachable from a shell, HTTP or CI

  • What. opencode’s code mode runs only when a model calls execute inside a session; no CLI command or HTTP route invokes it. mcpx runs scripts from the shell, /v1/exec and MCP; lootbox from lootbox exec and its WebSocket.
  • Where. mcpx; lootbox; Cloudflare (from the Worker’s own code). opencode v1 and v2: model turn only.
  • mcpx @ 05c78b2. mcpx exec/mcpx run, POST /v1/exec (internal/daemon/routes_exec.go:36), and mcpx_exec over MCP (internal/mcpserver/server.go:327).
  • Value to mcpx. + high: scripting from shell, CI, cron or another agent has no opencode equivalent; this is one of the two things OPENCODE-V2.md says to keep mcpx for (OPENCODE-V2.md:218-220).
  • Effort. S — done.
  • Risk. None.
  • Detail. The only mention of code-mode execution in v2’s protocol code is a simulation schema, and POST /api/generate is text generation (v2:packages/protocol/src/groups/generate.ts:8); rg over v2:packages/server/src and v2:packages/cli/src finds only an ACP tool-kind mapping (v2:packages/cli/src/acp/tool.ts:15). The doc’s claim that nothing outside a model turn can invoke it (OPENCODE-V2.md:62-67) holds at the pin. Serving code execution as an MCP server (mcpx /mcp, Cloudflare codeMcpServer and openApiMcpServer; lootbox never) is the plugin-API register’s PLG-41.
  • Sources. v2:packages/protocol/src/simulation.ts:500; v2:packages/protocol/src/groups/generate.ts:8; v1:packages/opencode/src/tool/registry.ts:118-119; internal/daemon/routes_exec.go:36

Completion

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  completion/complete per revision (capability, references, context, limits), what mcpx answers and forwards, and what opencode sends

completion/complete suggests values for a prompt argument or a resource-template variable as a user types. The method is in every revision, while the completions capability (2025-03-26; CAP-13 in the capabilities register), context.arguments (2025-06-18) and a schema-enforced 100-value limit (2026) came later. What matters most at 05c78b2: mcpx’s MCP server ignores the reference and answers with its own tool names although the daemon can already forward the request to the upstream that owns the prompt, and neither opencode version ever sends the request.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
CMP-01method exists in 2024-11-05 with no gating capability2024-11-05 has mcpx missing24-11 ✓ (ungated) · 25-03 ✓ (gated) · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — served in every era; a 2024-11-05 upstream is never asked (#213)+ lowSlow
CMP-02calling without the capability → -326012025-03-26 has25-03 ✓ onward (prose)✓ — client does not call undeclared upstreams+ lowSlow
CMP-03ref/prompt and ref/resource; ResourceReference renamed, wire unchanged2024-11-05 has 2025-06-18 hasall five (TS rename 25-06; 26-07 prose: uri may be a template)✓ — wire unchanged; /v1/complete accepts both+ lowSlow
CMP-04PromptReference.title2025-06-18 has25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — tolerated; dropped when forwarded+ lowSlow
CMP-05context.arguments: previously resolved argument values2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — ignored by the MCP server, not forwarded by /v1 (#213)+ medSmed
CMP-06at most 100 values; schema-enforced from 20262024-11-05 has 2026-07-28 has24-11..25-11 prose · 26-07 @maxItems 100✓ — own replies capped at 100 by default+ lowSlow
CMP-07total and hasMore (no cursor)2024-11-05 hasall five ✓✓ — computed correctly for what mcpx returns+ lowSlow
CMP-08completion is neither MRTR-capable nor cacheable2026-07-28 has26-07 ✓✓ — n/a in practice+ lowSlow
CMP-09mcpx ignores ref and completes from its own tool names2024-11-05 has mcpx missingmcpx only✗ — prompt argument got tool names (wire W24) (#213)+ medSmed
CMP-10MCP surface never forwards upstream, though the daemon canmcpx missingmcpx only✗ — Pool.Complete unused by /mcp (#213)+ medSlow
CMP-11neither opencode version sends completion/completemcpx has, others don'topencode v1 — · v2 —n/a — mcpx’s completion serves other hosts+ lowSlow

CMP-01 completion/complete without a capability in 2024-11-05

  • What. The method, its ref, argument {name, value} and result completion {values, total?, hasMore?} are all in 2024-11-05; no capability gates them. 2025-03-26 adds completions, which servers supporting the method MUST declare.
  • Where. Method in all five; capability from 2025-03-26.
  • mcpx @ 05c78b2. As a server it answers in every era (internal/mcpserver/server.go:558-563) and declares completions in every era it serves; it does not serve 2024-11-05 (internal/mcpserver/server.go:778). As a client it calls completion/complete only when the upstream declared completions (internal/mcpclient/request.go:50), so a 2024-11-05 upstream — which cannot declare it — is never asked, even if it implements the method.
  • Value to mcpx. + low: accept liberally by trying the method on a 2024-11-05 upstream and treating -32601 as “unsupported”.
  • Effort. S.
  • Risk. A 2024-11-05 server that implements completion looks like one that does not.
  • Detail. The gate’s comment explains why it exists: a well-behaved server answers method-not-found and “the rest” answer unpredictably. For 2024-11-05 that reasoning inverts, because declaring was impossible.
  • Sources. schema/2024-11-05/schema.ts:954 “export interface CompleteRequest extends Request {”; 2025-03-26/changelog.mdx:26 “Added completions capability to explicitly indicate support for argument”; internal/mcpclient/request.go:50 “if !c.Supports(”; internal/mcpserver/server.go:558-563

CMP-02 capability-less call → -32601

  • What. From 2025-03-26 the prose error list includes “Method not found: -32601 (Capability not supported)”.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. As a server mcpx always declares and always answers, so it never needs this code. As a client it refuses to call an upstream that did not declare completions and reports ok=false so the caller can fall back (internal/mcpclient/request.go:50).
  • Value to mcpx. + low; nothing to change.
  • Effort. None.
  • Risk. None.
  • Detail. mcpx deliberately answers rather than returning -32601, because “a client offering completion and receiving method-not-found simply shows nothing, and the user concludes the feature is broken” (internal/mcpserver/server.go:559-562). The cost of that choice is CMP-09.
  • Sources. 2025-03-26/server/utilities/completion.mdx:130 “- Method not found: -32601 (Capability not supported)”; internal/mcpclient/request.go:50; internal/mcpserver/server.go:559-562

CMP-03 reference types

  • What. ref is {type: "ref/prompt", name} or {type: "ref/resource", uri}. The TypeScript name of the resource variant changes from ResourceReference to ResourceTemplateReference in 2025-06-18 — type and uri unchanged. 2026 prose says the uri may be a URI template.
  • Where. All five; the rename is in no changelog.
  • mcpx @ 05c78b2. The MCP server ignores ref entirely (CMP-09). /v1/complete accepts both types and rejects any other with 400 (internal/daemon/ops.go:339-343).
  • Value to mcpx. + none on the wire.
  • Effort. None.
  • Risk. None.
  • Detail. The rename signals that ref/resource names a template, which is what uri held in practice.
  • Sources. schema/2025-03-26/schema.ts:1138 “export interface ResourceReference {”; schema/2025-06-18/schema.ts:1369 “export interface ResourceTemplateReference {”; 2026-07-28/server/utilities/completion.mdx:140 “References a resource URI or URI template”; internal/daemon/ops.go:339-343

CMP-04 PromptReference.title

  • What. PromptReference extends BaseMetadata, so a reference may carry title beside name.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. Tolerated. /v1/complete re-marshals ref from type, name and uri only (internal/daemon/ops.go:314-318, internal/daemon/ops.go:355), so a title is dropped before an upstream sees it.
  • Value to mcpx. + none.
  • Effort. None.
  • Risk. None.
  • Detail. A display title on a request reference carries no meaning for the server; ignoring it is right.
  • Sources. schema/2025-06-18/schema.ts:1384 “export interface PromptReference extends BaseMetadata {”; internal/daemon/ops.go:355

CMP-05 context.arguments

  • What. An optional context: { arguments?: {[name]: string} } on the request carries the values already chosen for other arguments, so a multi-argument prompt or template can complete dependent values (the branch after the repo).
  • Where. 2025-06-18 onward. Values are strings only.
  • mcpx @ 05c78b2. The MCP server parses only argument (internal/mcpserver/server.go:1556-1562). /v1/complete builds the upstream request from ref and argument alone (internal/daemon/ops.go:355); its own context field is mcpx’s call context (session, cwd), a different thing that happens to share the name (internal/daemon/ops.go:323).
  • Value to mcpx. + correct dependent completion once requests are forwarded (CMP-10).
  • Effort. S — accept context.arguments and pass it through.
  • Risk. If not done: upstreams suggest values that ignore earlier choices. The name clash on /v1 invites a caller to put arguments in the wrong context.
  • Detail. None beyond the above.
  • Sources. schema/2025-06-18/schema.ts:1333 “context?: {”; 2025-06-18/server/utilities/completion.mdx:79 “clients should include previous completions in the context.arguments object”; internal/daemon/ops.go:355 “params, _ := json.Marshal(map[string]any{”; internal/daemon/ops.go:323 “Context config.CallContext `”

CMP-06 the 100-value limit

  • What. A reply may carry at most 100 values. Through 2025-11-25 that is prose and a doc comment; 2026 adds a machine-checkable @maxItems 100.
  • Where. Limit in all five; schema constraint in 2026.
  • mcpx @ 05c78b2. Its own replies are cut to completion.maxValues (default 100; internal/mcpserver/server.go:1583-1586). The setting’s help says “higher is not” legal, but nothing clamps it (internal/settings/wire.go:179-183). /v1/complete passes an upstream’s completion through without applying the cap (internal/daemon/ops.go:362-374).
  • Value to mcpx. + truncate upstream replies before they reach a 2026 host, once CMP-10 forwards them.
  • Effort. S.
  • Risk. A value above 100, from the setting or a sloppy upstream, fails 2026 schema validation.
  • Detail. The MCP server and /v1 read the same setting; a comment records that the MCP side used to hard-code 100 (internal/mcpserver/server.go:1579-1582).
  • Sources. 2024-11-05/server/utilities/completion.mdx:78 “- Maximum 100 items per response”; schema/2026-07-28/schema.ts:2649 “@maxItems 100”; internal/settings/wire.go:179-183; internal/mcpserver/server.go:1583-1586; internal/daemon/ops.go:362-374

CMP-07 total and hasMore

  • What. total may exceed the number of values sent; hasMore says more exist even when the total is unknown. There is no cursor: the client narrows by typing more.
  • Where. All five, unchanged.
  • mcpx @ 05c78b2. Both the MCP server and the /v1 cache fallback set total to the full match count and hasMore to whether values were cut (internal/mcpserver/server.go:1587-1598, internal/daemon/ops.go:417-421).
  • Value to mcpx. + low.
  • Effort. None.
  • Risk. None.
  • Detail. Correct for what is computed; what is computed is the problem (CMP-09).
  • Sources. schema/2024-11-05/schema.ts:986 “total?: number;”; schema/2024-11-05/schema.ts:990 “hasMore?: boolean;”; internal/mcpserver/server.go:1587-1598

CMP-08 neither MRTR-capable nor cacheable

  • What. In 2026 CompleteRequestParams extends plain RequestParams (not InputResponseRequestParams) and CompleteResult extends Result (not CacheableResult).
  • Where. 2026.
  • mcpx @ 05c78b2. Nothing to do: mcpx never answers completion with input_required and adds no cache hints.
  • Value to mcpx. + no work.
  • Effort. None.
  • Risk. None.
  • Detail. An upstream that would need user input to complete has no protocol path; it can only return nothing.
  • Sources. schema/2026-07-28/schema.ts:2593 “export interface CompleteRequestParams extends RequestParams {”; schema/2026-07-28/schema.ts:2644 “export interface CompleteResult extends Result {”

CMP-09 mcpx ignores ref and completes tool names

  • What. Whatever the reference — a prompt argument, a resource-template variable — mcpx’s MCP server answers with its own tool and namespace names that contain the typed text.
  • Where. mcpx only. docs/protocol.md:86 documents it: “from mcpx’s own names”.
  • mcpx @ 05c78b2. complete parses only argument and matches against s.Tools() (internal/mcpserver/server.go:1555-1599). Wire W24: completing prompt fake_summarise, argument style, value b, returned ["mcpx_ask_abandon","mcpx_ask_begin","mcpx_globals"].
  • Value to mcpx. + real suggestions; − today’s output is noise for every real use.
  • Effort. S — forward to the owning upstream when ref names one (CMP-10).
  • Risk. Hosts show nonsense suggestions, and users conclude completion is broken — the outcome the design meant to avoid.
  • Detail. The completions declaration is honest in that mcpx answers; it is not honest about what the answers are.
  • Sources. internal/mcpserver/server.go:1556-1562; internal/mcpserver/server.go:1555-1599; docs/protocol.md:86

CMP-10 the MCP surface never forwards upstream

  • What. /v1/complete forwards completion/complete to the server that owns the prompt or template through Pool.Complete when that server declared completions, and falls back to cached names otherwise, saying which in a source field. The MCP completion/complete branch never uses it.
  • Where. mcpx internal parity (CLI = /v1 = MCP).
  • mcpx @ 05c78b2. internal/pool/request.go:60-70; internal/daemon/ops.go:357; the MCP branch (internal/mcpserver/server.go:558-563) answers locally.
  • Value to mcpx. + real argument values from upstream prompts and templates, through the protocol hosts already speak.
  • Effort. S — split the namespaced prompt name back into <ns> and <name>, as GetPrompt already does (internal/cli/serve.go:593-594), and strip mcpx://<ns>/ from template URIs.
  • Risk. None.
  • Detail. The /v1 cache fallback has its own bug: for ref/resource it matches t.URI (internal/daemon/ops.go:405-409), but cached templates carry their template in URITemplate and leave URI empty (internal/pool/pool.go:562-571), so a non-empty prefix matches nothing and an empty one returns empty strings. For ref/prompt the fallback suggests prompt names, not argument values.
  • Sources. internal/pool/request.go:60 “func (p *Pool) Complete(ctx context.Context, sessionKey string, params json.RawMessage) (json.RawMessage, bool, error) {”; internal/daemon/ops.go:357; internal/daemon/ops.go:405-409; internal/pool/pool.go:562-571

CMP-11 opencode never sends completion/complete

  • What. Neither opencode client calls completion/complete (nor resources/subscribe or logging/setLevel).
  • Where. opencode v1 and v2 lack it; their MCP client interfaces have no completion method.
  • mcpx @ 05c78b2. n/a — mcpx’s completion (CMP-09, CMP-10) serves other hosts.
  • Value to mcpx. + low: fixing CMP-09 improves mcpx for other hosts, not opencode.
  • Effort. None.
  • Risk. None.
  • Detail. v2 on a modern connection can still open subscriptions/listen through its SDK for list changes.
  • Sources. v1:packages/opencode/src/mcp/index.ts:164; v2:packages/core/src/mcp/client.ts:91

Content types

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Every MCP content type and result-content field per revision, and what mcpx, opencode, lootbox and Cloudflare do with each.

A tool result, a prompt message and a sampling message carry typed content blocks: text, image, audio, embedded resource, resource_link, and, inside sampling only, tool_use and tool_result; each may carry annotations and _meta, and a tool result may add structuredContent. mcpx has a per-revision downgrade() that rewrites these for older clients, but in production only two of its five branches can fire; what decides what a host actually sees is that mcpx_call renders every upstream result to one text block (dropping images, audio, resources, _meta and isError), so the only non-text block mcpx ever sends is the resource_link for an mcpx_exec artifact.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
CT-01text content block2024-11-05 has opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ (+_meta) · 25-11 ✓ · 26-07 ✓✓ — the only block mcpx_call emits+ highSlow
CT-02image content block2024-11-05 has opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 attachment · v2 mediapartial — mcpx_call drops it; scripts keep it+ medMmed
CT-03audio content block2025-03-26 has opencode v2 has24-11 — · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 dropped · v2 mediapartial — dropped by mcpx_call; downgrade branch dead+ lowSlow
CT-04Embedded resource block (tool results and prompts only)2024-11-05 has opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · never in samplingpartial — dropped by mcpx_call; downgrade target+ medSlow
CT-05resource_link block2025-06-18 has opencode v2 has25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 direct dropped · v2 uri text✓ — emitted for mcpx_exec artifacts only+ highSmed
CT-06resource_link rewritten as embedded resource below 2025-06-182025-06-18 has mcpx missingmcpx only (downgrade for 2025-03-26 clients)partial — broken mime: link type over text label (#206)+ lowSmed
CT-07ContentBlock union and where each type is allowed2025-06-18 has specs conflict24-11 inline unions · 25-06 ContentBlock · prompts prose lags to 26-07✓ — gated per revision in downgrade()+ lowSlow
CT-08tool_use / tool_result blocks, sampling only2025-11-25 has 2026-07-28 deprecates25-11 ✓ · 26-07 depn/a — not examined− nicheMlow
CT-09annotations on content (audience, priority, lastModified)2024-11-05 has 2025-06-18 has24-11 Annotated · 25-03 Annotations · 25-06 +lastModified · 26-07 ✓partial — passed by downgrade(), dropped by mcpx_call+ medSlow
CT-10_meta on content blocks2025-06-18 has25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — dropped by mcpx_call, link rewrite, MRTR render+ lowSlow
CT-11ToolResultContent.structuredContent becomes any JSON2026-07-28 has25-11 object · 26-07 any JSON (sampling)n/a — not examined+ lowSlow
CT-12What downgrade() touches, and which branches can firemcpx has, others don'tmcpx onlypartial — only link and resultType branches live+ medSmed
CT-13mcpx_call renders every result to one text block2025-06-18 has mcpx missingmcpx /mcp and stdio mcpx servepartial — images, audio, resources, _meta dropped (#206)+ medMhigh
CT-14Unknown content block type fails the whole callopencode v1 has opencode v2 hasopencode v1 verified · v2 same union✓ — never forwards an unknown block+ medShigh
CT-15Script clients unwrap tool resultshas better replacement opencode v1 has opencode v2 hasmcpx · opencode v1/v2 · Cloudflare unwrap · lootbox raw envelope✓ — structuredContent, then text, then blocks+ highSlow
CT-16JSON arriving as text parsed into a valueopencode v2 hasmcpx always by shape · opencode v2 without outputSchema · v1 never✓ — parses {…}/[…] text+ lowSlow
CT-17Full envelope kept on .rawmcpx has, others don'tmcpx only✓ — non-enumerable .raw on every value+ medSlow
CT-18Media produced by a tool inside a scriptopencode v1 has opencode v2 hasopencode attaches to model · mcpx artifact link · lootbox base64 · CF mixedpartial — plugin never declares artifacts+ medSmed
CT-19isError: true becomes an exception in scriptshas better replacement opencode v1 has opencode v2 hasmcpx · opencode v1/v2 · Cloudflare throw · lootbox returns it✓ — ToolError with .raw+ highSlow

CT-01 text content block

  • What. {type: "text", text, annotations?, _meta?}.
  • Where. All five revisions; allowed in tool results, prompt messages and sampling messages. _meta from 2025-06-18. opencode v1 and v2 pass it to the model as text.
  • mcpx @ 05c78b2. The one block mcpx_call ever emits (CT-13); also what downgrade() renders structuredContent and audio into for older clients (internal/mcpserver/revisions.go:146-156, :224-226).
  • Value to mcpx. + high: the baseline every host reads.
  • Effort. S (nothing to do).
  • Risk. None.
  • Detail. Everything mcpx cannot express for a client ends up here, so text is also where information is lost silently: a text block has no mime type and no URI.
  • Sources. schema/2024-11-05/schema.ts:846 “type: "text";”; schema/2026-07-28/schema.ts:2317 “type: "text";”; v2:packages/core/src/mcp/client.ts:324

CT-02 image content block

  • What. {type: "image", data (base64), mimeType, annotations?, _meta?}.
  • Where. All five revisions; tool results, prompts and sampling. opencode v1 turns it into a data-URL file attachment; v2 into a media part. Both deliver it to a vision model.
  • mcpx @ 05c78b2. mcpx_call drops it (renderResult keeps only text, internal/cli/commands.go:385-415); if a result has no text block at all, the entire raw result JSON, base64 included, becomes the text (internal/cli/commands.go:414). Scripts keep images reachable through the value and .raw (CT-15, CT-17). The MRTR result render also drops images (see the MRTR area).
  • Value to mcpx. + med: a screenshot tool behind mcpx_call either loses the picture or floods the context with base64. − text keeps context small, which is mcpx’s goal; mcpx_exec artifacts (CT-05) are the designed answer.
  • Effort. M — pass image blocks through, or turn them into artifacts the way mcpx_exec does.
  • Risk. Med: megabytes of base64 in a model’s context when the upstream returns only an image.
  • Detail. The raw-JSON fallback is the worst case: an image-only result produces no text, so renderResult returns string(raw).
  • Sources. schema/2024-11-05/schema.ts:857 “type: "image";”; internal/cli/commands.go:414 “return string(raw)”; v1:packages/opencode/src/session/tools.ts:430; v2:packages/core/src/mcp/client.ts:325-326

CT-03 audio content block

  • What. {type: "audio", data (base64), mimeType}.
  • Where. 2025-03-26 onward; tool results, prompts and sampling. opencode v2 treats it as media; opencode v1’s direct path drops it silently (its converter handles only text, image and resource), while v1 code mode attaches it as a file.
  • mcpx @ 05c78b2. downgrade() has an audio→text branch (“(audio mime, base64, elided)”), but FeatAudio’s floor equals Oldest (2025-03-26), and every version mcpx agrees to is at or above it, so the branch cannot fire; a 2024-11-05 client is refused at initialize before it could need it. mcpx emits no audio anyway, and mcpx_call drops upstream audio.
  • Value to mcpx. − dead code today. + becomes live if mcpx ever serves 2024-11-05.
  • Effort. S.
  • Risk. Serving 2024-11-05 without extending the floors table would send 2025-03-26 shapes (audio, tool annotations, the completions capability) to 2024-11-05 clients.
  • Detail. The comment at internal/mcpserver/revisions.go:45-48 says the branch is kept deliberately (“always true today” is a fact about the floor). describeBlob appends “base64, elided” only when data was non-empty.
  • Sources. schema/2025-03-26/schema.ts:991 “type: "audio";”; 2025-03-26/changelog.mdx:25 “Added support for audio data, joining the existing text and image content types”; internal/mcpserver/revisions.go:87 “FeatAudio: "2025-03-26",”; internal/mcpserver/revisions.go:14; v1:packages/opencode/src/session/tools.ts:429

CT-04 Embedded resource block (tool results and prompts only)

  • What. {type: "resource", resource: TextResourceContents | BlobResourceContents, annotations?, _meta?}: the resource’s contents inline.
  • Where. All five revisions in tool results and prompt messages. Never a top-level sampling block in any revision (from 2025-11-25 only nested inside tool_result.content). opencode v1 turns text into text and a blob into an attachment only for PDF, GIF, JPEG, PNG and WebP up to 10 MiB, else a “[Binary MCP resource omitted …]” line; v2 turns text into text and a blob into media, or into its URI when the blob has no mimeType.
  • mcpx @ 05c78b2. The target of the resource_link downgrade (CT-06). mcpx_call drops upstream embedded resources.
  • Value to mcpx. + med: the only way before 2025-06-18 to hand a client something with a URI.
  • Effort. S.
  • Risk. Low.
  • Detail. In 2024-11-05 the sampling message union is TextContent | ImageContent only, so a resource can never be sampled directly.
  • Sources. schema/2024-11-05/schema.ts:617 “type: "resource";”; schema/2024-11-05/schema.ts:812 “content: TextContent | ImageContent;”; v1:packages/opencode/src/session/tools.ts:32 “const MAX_MCP_RESOURCE_BLOB_BYTES = 10 * 1024 * 1024”; v1:packages/opencode/src/session/tools.ts:436; v2:packages/core/src/mcp/client.ts:328
  • What. ResourceLink extends Resource plus type: "resource_link": a URI reference with name, mime type and description instead of inline contents. A link returned by a tool is not guaranteed to appear in resources/list.
  • Where. 2025-06-18 onward in tool results and prompts (and inside sampling tool_result.content from 2025-11-25). opencode v2 renders it as its bare uri in a text part (the name is lost). opencode v1 drops it silently on the direct path and renders "<name>: <uri>" in code mode.
  • mcpx @ 05c78b2. The only non-text block mcpx emits: mcpx_exec artifacts become resource_link blocks (uri mcpx://artifacts/{id}, name, mimeType, description “N bytes, produced by this script”, internal/cli/serve.go:174-187). They travel through the backend’s string result behind a U+001E marker and are re-encoded in tools/call (internal/mcpserver/toolresult.go:21, :34-56; internal/mcpserver/server.go:676-681).
  • Value to mcpx. + high: keeps binaries out of context, the reason mcpx_exec exists.
  • Effort. S (done).
  • Risk. Med: opencode v1 without code mode never shows the model an mcpx_exec artifact; the text part of the result should mention the URI for such hosts.
  • Detail. description on a link is valid because ResourceLink extends Resource. The prompts prose documents links only from 2026-07-28 although the 2025-06-18 schema allows them (CT-07).
  • Sources. schema/2025-06-18/schema.ts:761 “type: "resource_link";”; schema/2026-07-28/schema.ts:1712 “Note: resource links returned by tools are not guaranteed to appear in the results of”; internal/cli/serve.go:180 “"type": "resource_link",”; v2:packages/core/src/mcp/client.ts:327 “if (part.type === "resource_link") return [{ type: "text", text: part.uri }]”; v1:packages/opencode/src/tool/code-mode.ts:104
  • What. For a 2025-03-26 client, linkAsResource turns a link into {type: "resource", resource: {uri, mimeType: <link's mimeType>, text: "name — uri"}}, in tool results and inside a prompt message’s single block.
  • Where. mcpx only.
  • mcpx @ 05c78b2. internal/mcpserver/revisions.go:220-223 (dispatch), :248-253 (prompt messages), :260-273 (the rewrite). For a PNG artifact the client receives mimeType: "image/png" over a text body that is a label.
  • Value to mcpx. + low: text/plain for the label, with the original type in the text or _meta, would be honest.
  • Effort. S.
  • Risk. Med: a 2025-03-26 host that trusts mimeType tries to render a label as an image.
  • Detail. text/plain is used only when the link has no mimeType (internal/mcpserver/revisions.go:264-266), so the label is typed honestly only for untyped links; every typed artifact is mislabelled. The rewrite keeps uri machine-readable (its purpose) but drops the link’s description, title, size, annotations and _meta. It is one of the two downgrade() branches that can fire in production (CT-12).
  • Sources. internal/mcpserver/revisions.go:271-272 “"uri": uri, "mimeType": mime, "text": text}}”; schema/2025-03-26/schema.ts:654

CT-07 ContentBlock union and where each type is allowed

  • What. 2025-06-18 names the union TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource and uses it for CallToolResult.content[] and PromptMessage.content; earlier revisions spell inline unions.
  • Where. Allowed blocks per location (T text, I image, A audio, R embedded resource, L link, U tool_use, X tool_result): CallToolResult.content[] — 24-11 T I R; 25-03 T I A R; 25-06 onward T I A R L. PromptMessage.content (one block, never an array) — same progression. SamplingMessage.content — 24-11 T I; 25-03/25-06 T I A; 25-11 T I A U X, single or array; 26-07 same, deprecated. ToolResultContent.content[] (nested in sampling) — 25-11 onward T I A R L. ReadResourceResult.contents[] is Text|BlobResourceContents, never content blocks.
  • mcpx @ 05c78b2. downgrade() gates resource_link and structuredContent by revision (internal/mcpserver/revisions.go:146); audio is gated but never fires (CT-03).
  • Value to mcpx. + low: the table is what downgrade() must encode.
  • Effort. S.
  • Risk. Low.
  • Detail. Prose and schema disagree for prompts: the 2025-06-18 schema allows resource_link in prompt messages, but the 2025-06-18 and 2025-11-25 prompts pages document only text, image, audio and embedded resource; a “Resource Links” section first appears in 2026-07-28, and the 2025-06-18 changelog advertises links only “in tool call results” (prompts area).
  • Sources. schema/2025-06-18/schema.ts:1133 “export type ContentBlock =”; schema/2024-11-05/schema.ts:656 “content: (TextContent | ImageContent | EmbeddedResource)[];”; schema/2025-03-26/schema.ts:699 “content: (TextContent | ImageContent | AudioContent | EmbeddedResource)[];”; schema/2025-06-18/schema.ts:750 “content: ContentBlock;”; schema/2025-11-25/schema.ts:1683 “content: SamplingMessageContentBlock | SamplingMessageContentBlock[];”; schema/2025-11-25/schema.ts:1884 “content: ContentBlock[];”; 2026-07-28/server/prompts.mdx:274 “#### Resource Links”

CT-08 tool_use / tool_result blocks, sampling only

  • What. ToolUseContent {id, name, input, _meta?} and ToolResultContent {toolUseId, content: ContentBlock[], structuredContent?, isError?, _meta?}, allowed only in SamplingMessageContentBlock.
  • Where. 2025-11-25; 2026-07-28 keeps them but deprecates sampling.
  • mcpx @ 05c78b2. Not examined (the sampling area covers what mcpx relays).
  • Value to mcpx. − matters only for a sampling relay.
  • Effort. M.
  • Risk. Low.
  • Detail. Neither carries annotations; clients SHOULD preserve their _meta (it is used for caching).
  • Sources. schema/2025-11-25/schema.ts:1693 “export type SamplingMessageContentBlock =”; schema/2025-11-25/schema.ts:1835 “type: "tool_use";”; schema/2025-11-25/schema.ts:1869 “type: "tool_result";”

CT-09 annotations on content (audience, priority, lastModified)

  • What. annotations: {audience?, priority? (0..1), lastModified? (ISO 8601)} on text, image, audio, embedded resource and link blocks.
  • Where. 2024-11-05 through a mixin (extends Annotated); 2025-03-26 renames it to an explicit annotations?: Annotations field (same wire shape, not in the changelog); 2025-06-18 adds lastModified (changelog-silent). Never on tool_use/tool_result.
  • mcpx @ 05c78b2. downgrade() never touches annotations, so they pass through for any revision; mcpx_call drops them with everything else that is not text (CT-13); the link rewrite drops the link’s annotations (CT-06).
  • Value to mcpx. + med: audience: ["user"] is the spec’s own way to keep a block out of model context, which is mcpx’s goal.
  • Effort. S.
  • Risk. Low.
  • Detail. lastModified would be sent unchanged to a 2025-03-26 client, which does not define it; harmless to a client that ignores unknown fields, and one more field downgrade() does not police.
  • Sources. schema/2024-11-05/schema.ts:818 “export interface Annotated {”; schema/2025-03-26/schema.ts:924 “export interface Annotations {”; schema/2025-06-18/schema.ts:1127 “lastModified?: string;”

CT-10 _meta on content blocks

  • What. TextContent, ImageContent, AudioContent, EmbeddedResource and ResourceLink (through Resource) gain _meta.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. Dropped by mcpx_call (CT-13), by the link rewrite (CT-06) and by the MRTR result render (MRTR area). downgrade() leaves it in place for 2025-03-26 clients, which do not define it.
  • Value to mcpx. + low: extensions (MCP Apps UI pointers, for example) ride on _meta.
  • Effort. S.
  • Risk. Low today.
  • Detail. Result-level _meta is recorded in the _meta area; this row is the per-block field.
  • Sources. schema/2025-06-18/schema.ts:1157 “_meta?: { [key: string]: unknown };”

CT-11 ToolResultContent.structuredContent becomes any JSON

  • What. The sampling tool_result block’s structuredContent is an object in 2025-11-25 and unknown in 2026-07-28, mirroring TOOL-27.
  • Where. 2025-11-25 object; 2026-07-28 any JSON. The 2026 changelog mentions only the CallToolResult field.
  • mcpx @ 05c78b2. Not examined.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. A sampling relay forwarding a 2026 host’s tool results to a 2025-11-25 server would hit the same non-object gap as TOOL-27.
  • Sources. schema/2025-11-25/schema.ts:1891 “structuredContent?: { [key: string]: unknown };”; schema/2026-07-28/schema.ts:2469 “structuredContent?: unknown;”

CT-12 What downgrade() touches, and which branches can fire

  • What. downgrade(result, version) runs on every successful result. At 2026-07-28 it returns the result untouched. Otherwise, on a JSON copy: below 2025-06-18 it moves structuredContent into a text block (TOOL-27) and rewrites resource_link (CT-06); below 2025-03-26 it rewrites audio (CT-03); below 2026-07-28 it deletes resultType (LV-36). The same content rewrite applies to each prompt message’s single block.
  • Where. mcpx only; opencode and lootbox never rewrite for a peer.
  • mcpx @ 05c78b2. internal/mcpserver/revisions.go:132-170, 206-232, 236-258, 260-289. mcpx emits no structuredContent and no audio, so only the link and resultType branches can fire.
  • Value to mcpx. + med: the single edge is sound design; it is also where every other send-conservatively fix belongs.
  • Effort. S.
  • Risk. Med: it never looks at text, image, resource, annotations, _meta, icons, titles, or any list result (tools/list, prompts/list, …). When mcpx emits richer results, fields a client’s revision lacks will leak; titles in prompts/list to 2025-03-26 already do (prompts area).
  • Detail. The fast path’s condition is “the version defines structuredContent, resource_link, audio and resultType”, that is 2026-07-28 only; 2025-06-18 and 2025-11-25 results are still copied, although at most resultType changes.
  • Sources. internal/mcpserver/revisions.go:133-138; internal/mcpserver/server.go:439 “resp.Result = downgrade(resp.Result, peer.Version)”

CT-13 mcpx_call renders every result to one text block

  • What. The core proxy tool returns one text block. renderResult decodes only content[].type/text, structuredContent and isError: if structuredContent is present it wins, pretty-printed; else text blocks are joined with newlines; else the whole raw result JSON is the text. Images, audio, embedded resources, links, annotations and upstream _meta are dropped.
  • Where. mcpx’s MCP surface (/mcp and stdio mcpx serve). /v1/call and scripts get the raw result.
  • mcpx @ 05c78b2. internal/cli/commands.go:385-415; called from mcpBackend.Call (internal/cli/serve.go:103-117). Wire W21: an upstream structuredContent {"n": 42} reached the host as the text "{\n \"n\": 42\n}".
  • Value to mcpx. + med: images to multimodal hosts (as blocks, or as artifacts as mcpx_exec does) and _meta for extensions. − text-only is cheap, which is mcpx’s goal.
  • Effort. M.
  • Risk. High: an image-only upstream result becomes megabytes of base64 text (CT-02); a failure becomes a success (TOOL-26).
  • Detail. The function’s comment says it unwraps “the same way the script client does, so CLI and script output agree”; the script client throws on isError and keeps images on .raw, so the two do not agree. downgradeMessages/downgradeContent therefore only ever see one text block from mcpx_call.
  • Also recorded from the tools register. renderResult (internal/cli/commands.go:385-415), falling back to string(raw) (internal/cli/commands.go:414). Wire W21: upstream structuredContent {n: 42} arrived as the text "{\n \"n\": 42\n}" with no structuredContent. renderResult’s comment says it unwraps “the same way the script client does”, but the script client throws on isError and keeps the untouched envelope on .raw (code-mode register); mcpx_call does neither. A JSON null structuredContent is rendered as the text null.
  • Sources. internal/cli/commands.go:383-384 “// renderResult unwraps a CallToolResult the same way the script client does,”; internal/cli/commands.go:414 “return string(raw)”; internal/cli/serve.go:116 “return renderResult(res.Result), nil”; wire W21; internal/cli/commands.go:383 “renderResult unwraps a CallToolResult the same way the script client does,”; internal/cli/commands.go:385-415

CT-14 Unknown content block type fails the whole call

  • What. Both opencode SDKs define ContentBlock as a closed union (text | image | audio | resource_link | resource). In v1 the result is parsed against CallToolResultSchema, so any other type rejects the call as a zod error, not an MCP error.
  • Where. opencode v1 (verified); v2’s SDK has the same union (its validation path was not traced).
  • mcpx @ 05c78b2. downgrade() gates resource_link and structuredContent by revision (internal/mcpserver/revisions.go:146), and mcpx_call emits text only, so no unknown block reaches opencode today.
  • Value to mcpx. + med: mcpx must never pass a new or extension block type through verbatim to a host that did not negotiate it.
  • Effort. S.
  • Risk. High if broken: one unknown block from an upstream loses the whole result, text included.
  • Detail. This is the practical reason send-conservatively applies to content: a strict client does not skip what it cannot parse. downgrade()’s own comment makes the same point (“a content block it cannot parse is not a degraded result, it is an unreadable one”, internal/mcpserver/revisions.go:130-131).
  • Sources. v1:packages/opencode/src/mcp/catalog.ts:59 “CallToolResultSchema,”; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/types.js (L1131–L1137, L1296); https://unpkg.com/@modelcontextprotocol/core@2.0.0/dist/auth-CUe6YdwF.mjs (L678–L684)

CT-15 Script clients unwrap tool results

  • What. What a script gets back from a tool call. mcpx: throws on isError; else structuredContent if present; else all-text results joined (and JSON-parsed if they look like JSON, CT-16); else the block array. opencode v1 code mode: structuredContent (unless null), else joined text (links as "<name>: <uri>"), else a placeholder such as [2 images attached to the result]. opencode v2: structuredContent, else text (parsed when there is no outputSchema). Cloudflare codeMcpServer: compatibility toolResult, MCP errors, structuredContent, all-text, then the mixed result. lootbox: the raw CallToolResult; the script digs out content[0].text itself.
  • Where. mcpx, opencode v1/v2 and Cloudflare unwrap; lootbox does not.
  • mcpx @ 05c78b2. unwrap in the generated client (internal/codegen/emit.go:493-519).
  • Value to mcpx. + high: this is the good path for agents, and it is done.
  • Effort. S (done).
  • Risk. Low.
  • Detail. mcpx does not handle the pre-2024-11-05 compatibility toolResult field that Cloudflare still honours (no toolResult in internal). mcpx_call (CT-13) does not share this logic although its comment says it does.
  • Sources. internal/codegen/emit.go:501 “if (raw && raw.structuredContent !== undefined) {”; .lootbox/src/lib/rpc/execute_mcp.ts:68-72; .lootbox/src/lib/external-mcps/parse_mcp_schemas.ts:283-293; v1:packages/opencode/src/tool/code-mode.ts:109-115; v2:packages/core/src/tool/mcp.ts:96-106; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#codemcpserver “Returned MCP values are unwrapped in this order: compatibility toolResult, MCP errors, structuredContent, all-text content, then the original mixed-content result.”

CT-16 JSON arriving as text parsed into a value

  • What. mcpx parses all-text results that look like a JSON object or array, even when the tool declares an outputSchema. opencode v2 parses only when the tool has no outputSchema and the text starts with { or [. opencode v1 never parses.
  • Where. mcpx (always, by shape); opencode v2 (only without a schema); opencode v1 (never); Cloudflare (undocumented); lootbox (n/a, raw envelope).
  • mcpx @ 05c78b2. internal/codegen/emit.go:505-516.
  • Value to mcpx. + low: v2’s extra condition avoids returning a parsed object where structuredContent was expected; mcpx could adopt it once it generates return types from outputSchema.
  • Effort. S.
  • Risk. Low: prose that happens to be […] becomes an array; .raw keeps the original (CT-17).
  • Detail. mcpx checks the first and last characters ({…} or […]); v2 checks only the first and falls back to text on a parse error.
  • Sources. internal/codegen/emit.go:505-516; v2:packages/core/src/tool/mcp.ts:99-104 “Agents assume JSON returned as text is already an object, so parse it when the server declares no schema.”; v1:packages/opencode/src/tool/code-mode.ts:109-110

CT-17 Full envelope kept on .raw

  • What. mcpx attaches the untouched CallToolResult to every unwrapped value as a non-enumerable .raw property; strings are boxed so they can carry it. Image bytes, annotations and _meta stay reachable there.
  • Where. mcpx only. opencode projects the envelope away and moves media to attachments; Cloudflare returns the mixed result only when content is not all text; lootbox returns nothing but the envelope.
  • mcpx @ 05c78b2. attachRaw (internal/codegen/emit.go:477-491).
  • Value to mcpx. + med: ergonomic values without losing the protocol detail.
  • Effort. S (done).
  • Risk. Low: typeof a boxed string is "object", which surprises a model that tests typeof r === "string"; a frozen object cannot take the property and the error is swallowed (internal/codegen/emit.go:481).
  • Detail. JSON.stringify of a boxed string gives the string, so logging is unaffected.
  • Sources. internal/codegen/emit.go:486-488

CT-18 Media produced by a tool inside a script

  • What. When a call inside an opencode script returns image, audio or blob content, opencode attaches it to the execute result as data-URL files, so the model sees the image. mcpx leaves the bytes to the script and, when the caller declares the artifacts capability, replaces media at the output boundary with a resource_link to a stored artifact; artifact() stores one explicitly.
  • Where. opencode v1/v2 (attach); mcpx (store and link); lootbox (base64 in the envelope, seen only if printed); Cloudflare (mixed result object, no attachment channel documented).
  • mcpx @ 05c78b2. docs/exec.md:259-275; interceptMedia (internal/execsvc/execsvc.go:543-545); artifact() (internal/codegen/emit.go:1275).
  • Value to mcpx. − opencode’s approach is the context cost mcpx avoids (“one screenshot costs more than the rest of the task”). + where a vision model must see the image, opencode’s channel is the only one that renders it; mcpx needs a resources/read.
  • Effort. S.
  • Risk. Med: the plugin’s mcpx_exec never declares artifacts, so its images are not intercepted and must be printed as base64 to be seen (inferred from plugin/opencode/mcpx/daemon.ts:452-455).
  • Detail. Artifacts are served by a random 128-bit id, not the content hash, because a hash is guessable (docs/exec.md:200-207).
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:89-100; v2:packages/core/src/codemode/tool.ts:139-147; docs/exec.md:259-275

CT-19 isError: true becomes an exception in scripts

  • What. mcpx, opencode and Cloudflare throw when a tool result carries isError: true. lootbox returns it as a successful value, so the script must check .isError itself and usually does not.
  • Where. mcpx (ToolError with server, tool and .raw); opencode v1 (throws, surfaced as a ToolFailure diagnostic); opencode v2 (ToolFailure); Cloudflare (“MCP error results become thrown connector errors”); lootbox passes it through.
  • mcpx @ 05c78b2. internal/codegen/emit.go:494-499.
  • Value to mcpx. + high: done, and the reason scripts do not share TOOL-26’s bug.
  • Effort. S (done).
  • Risk. Low for mcpx; a lootbox script can report success after every call failed.
  • Detail. Every thrower builds the message from the text blocks joined with newlines, with a default when there are none: mcpx “<server>.<tool> returned isError”, opencode “MCP tool returned an error”.
  • Sources. internal/codegen/emit.go:494 “if (raw && raw.isError) {”; .lootbox/src/lib/rpc/execute_mcp.ts:60-72; v2:packages/core/src/tool/mcp.ts:78-85; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#mcpconnector “MCP error results become thrown connector errors.”

Elicitation

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-10-30T19:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  elicitation/create per revision (form, url, 2026 via MRTR), its schema and capability rules, and how mcpx, opencode and lootbox carry it

Elicitation is how a server asks the user something through the client: a flat form (2025-06-18), a URL to open (2025-11-25), and from 2026-07-28 a question embedded in an input_required result rather than a request of its own. It is the one client feature 2026 does not deprecate, and for mcpx it is central: every upstream question lands in mcpx’s broker and is either relayed to the host that made the call or answered by another audience. What matters most is that the relay works for legacy hosts and for 2026 hosts that keep an Mcp-Session-Id, but a correct stateless 2026 host gets -32603 from any eliciting upstream (ELI-17), and that mcpx never tells upstreams it can take URL mode although its broker does. The 2026 mechanism itself (InputRequiredResult, requestState, retries, and mcpx as an MRTR client of a modern upstream) is registered in mrtr.md.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
ELI-01elicitation/create and the elicitation client capability2025-06-18 has opencode v2 has24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (MRTR); opencode v2 ✓, v1 ✗, lootbox ✗✓ — both directions through the broker+ highSlow
ELI-02requestedSchema limited to flat primitive properties2025-06-18 has25-06 ✓ · 25-11 ✓ (+ arrays for multi-select) · 26-07 ✓✓ — forwarded verbatim; never validated+ lowSlow
ELI-03Three-way action accept / decline / cancel2025-06-18 has25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — cancel when nobody was asked+ lowSlow
ELI-04Enum schemas: enumNames → titled/untitled single and multi-select2025-11-25 has25-06 enum+enumNames · 25-11 rich · 26-07 rich✓ — titled single-select downgraded to enum+enumNames for pre-2025-11-25 hosts in paramsFor, non-boolean default stripped; multi-select refused whole for old hosts (no equivalent; stays with the broker) (#2078)+ lowMlow
ELI-05default on every primitive schema2025-11-25 has opencode v2 has25-06 boolean only · 25-11 all · 26-07 all; opencode v2 applies defaults✓ — forwarded unchanged+ lowSlow
ELI-06ElicitResult.content values may be string[]2025-11-25 has25-06 scalars · 25-11 + string[] · 26-07 + string[]✓ — answers relayed as raw JSON+ lowSlow
ELI-07requestedSchema.$schema allowed2025-11-25 has25-11 ✓ · 26-07 ✓✓ — forwarded verbatim− mootSlow
ELI-08mode field; omitted means form2025-11-25 has25-11 ✓ · 26-07 ✓✓ — stripped for pre-2025-11-25 hosts+ lowSlow
ELI-09URL-mode elicitation2025-11-25 has opencode v2 has25-11 ✓ · 26-07 ✓; opencode v2 renders a link field✓ — relayed only to url-declared hosts; as a client mcpx declares url only with a handler installed (mcpclient/modern.go), the pool installs it, and -32042-named URLs open through the broker (#2079)+ medSmed
ELI-10elicitationId added, then removed2025-11-25 has 2026-07-28 removes mcpx missing25-11 onlypartial — stripped for old hosts, leaked to 2026 hosts (#201)+ lowSlow
ELI-11notifications/elicitation/complete added, then removed2025-11-25 has 2026-07-28 removes opencode v2 has25-11 only; opencode v2 handles itpartial — consumed from upstreams, never sent to hosts+ lowSlow
ELI-12-32042 URLElicitationRequiredError added, then removed2025-11-25 has 2026-07-28 removes has better replacement25-11 only; 26-07 reserves the code✓ — handled from upstreams as a client (broker URL flow, one retry, #2079) and emitted as a server to 2025-11-25 url-declared hosts when a pending URL question is undeliverable, listing exactly those elicitations; never emitted at 2026 (#2080)+ medMmed
ELI-13Sensitive data: never → only via URL mode2025-11-25 has25-06 never · 25-11 URL only · 26-07 URL onlyn/a — relays; broker answers persist to SQLite+ lowSmed
ELI-142026 ElicitRequest embedded in InputRequiredResult, not a request2026-07-28 has26-07 only✓ — produced and answered+ medSlow
ELI-15Legacy delivery over Streamable HTTP: request on the POST’s SSE2025-06-18 has 2026-07-28 removes25-03..25-11 allowed · 26-07 forbidden✓ — works with a session (wire L1–L3)+ highSlow
ELI-16Legacy delivery over stdio: request on the pipe2025-06-18 has25-06 ✓ · 25-11 ✓✓ — interruptible calls run concurrently; not wire-driven+ highSlow
ELI-17Stateless 2026 host plus eliciting upstream2026-07-28 has mcpx missing26-07 only✗ — -32603 after 8 busy rounds (wire M1) (#201)+ highMhigh
ELI-18Naming the originating server in a relayed questionmcpx missingspec: client MUST show who asks; mcpx: legacy path onlypartial — legacy prefixed “(via mcpx)”; 2026 path raw (#201)+ medSmed
ELI-19Question from a shared upstream instanceimpl deferredmcpx onlypartial — goes to the broker, never inline+ lowLlow
ELI-20Questions raised inside mcpx_execimpl deferred opencode v2 has mcpx missingmcpx onlypartial — always the broker, never the host (#77)+ lowLlow
ELI-21Answering an elicitation from inside a scriptopencode v2 has mcpx missingopencode v2 UI form; mcpx designed, not built✗ — no onElicit; broker and policy only (#214)+ medMlow
ELI-22Client with no elicitation answereropencode v1 has lootbox hasopencode v1 and lootbox: -32601; mcpx: cancel✓ — declares, answers cancel when nobody is asked+ lowSlow
ELI-23Elicitation attributed to a session or a whole locationopencode v2 hasopencode v2: Location-global formsn/a — mcpx is the server here+ lowSlow

ELI-01 elicitation/create and the elicitation client capability

  • What. A server asks the user, through the client, for structured data: message plus requestedSchema. A client that can answer declares elicitation (an opaque object in 2025-06-18).
  • Where. 2025-06-18 onward; absent in 2024-11-05 and 2025-03-26. In 2026-07-28 it survives as an embedded input request (ELI-14). opencode v2 declares it and answers; opencode v1 comments it out; lootbox declares no capabilities.
  • mcpx @ 05c78b2. Both directions, through the broker. Downstream: sent only to hosts at ≥2025-06-18 that declared it (internal/mcpserver/conn.go:192). Upstream: always declares elicitation: {} (internal/mcpclient/modern.go:51) and answers cancel when nothing is installed to ask (internal/mcpclient/modern.go:129-131), which keeps the always-on declaration honest.
  • Value to mcpx. + high: this is the feature that lets an upstream stop and ask the user, and mcpx exists partly to route those questions.
  • Effort. S — done.
  • Risk. None as is.
  • Detail. 2025-06-18 ElicitRequest params are one inline object; 2025-11-25 splits them into ElicitRequestFormParams | ElicitRequestURLParams. Upstream questions become broker rows (elc-… ids), answered by a host inline (ELI-15, ELI-16, the 2026 path in mrtr.md), by the CLI, TUI or plugin, or by policy.
  • Sources. schema/2025-06-18/schema.ts:1459 “elicitation/create”; schema/2025-06-18/schema.ts:250 “elicitation?: object;”; 2025-06-18/changelog.mdx:24 “Add support for”; internal/mcpclient/modern.go:51; internal/mcpserver/conn.go:192; v2:packages/core/src/mcp/client.ts:143; v1:packages/opencode/src/mcp/index.ts:44 “// elicitation: {},”; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153.

ELI-02 requestedSchema limited to flat primitive properties

  • What. type: "object" with properties of string, number, boolean or enum schemas and an optional required list. No nesting and no arrays of objects, so any client can render it as a form.
  • Where. 2025-06-18 onward. 2025-11-25 adds array-typed multi-select enums (ELI-04).
  • mcpx @ 05c78b2. Forwards the schema verbatim in both eras: the modern path copies the upstream’s params (internal/mcpserver/ask.go:259); the legacy path rewrites only message, mode and elicitationId (internal/mcpserver/conn.go:382-407). mcpx does not check that an upstream kept to the subset.
  • Value to mcpx. + low: the restriction is what makes a CLI or TUI prompt feasible.
  • Effort. S.
  • Risk. low: an upstream sending a nested schema reaches the host unchanged, and the host decides.
  • Detail. StringSchema.format is one of email, uri, date, date-time. opencode v2 maps each property to a typed form field and turns arrays into multi-select, replacing machine titles such as “string with format email” with the description or key.
  • Sources. schema/2025-06-18/schema.ts:1485 “export type PrimitiveSchemaDefinition =”; 2025-06-18/client/elicitation.mdx:279 “Note that complex nested structures, arrays of objects”; internal/mcpserver/ask.go:259; v2:packages/core/src/mcp/index.ts:224.

ELI-03 Three-way action accept / decline / cancel

  • What. accept (with content in form mode), decline (an explicit no), cancel (dismissed without answering).
  • Where. 2025-06-18 onward, unchanged.
  • mcpx @ 05c78b2. Keeps the distinction: with no answerer installed the client replies cancel, “Cancel, not decline. Nobody was asked, so nobody said no.” (internal/mcpclient/modern.go:129-131).
  • Value to mcpx. + low: correct already.
  • Effort. S.
  • Risk. None.
  • Detail. In URL mode accept means the user consented to open the URL, not that the out-of-band flow finished (ELI-14). Sampling has no such three-way answer: mcpx turns a declined sampling question into an error (see sampling.md).
  • Sources. schema/2025-06-18/schema.ts:1550 “action: "accept" | "decline" | "cancel";”; internal/mcpclient/modern.go:130 “Cancel, not decline.”.

ELI-04 Enum schemas: enumNames → titled/untitled single and multi-select

  • What. 2025-06-18 has EnumSchema{enum, enumNames?}. 2025-11-25 (SEP-1330) replaces it with UntitledSingleSelect{enum}, TitledSingleSelect{oneOf[{const,title}]}, UntitledMultiSelect{type:"array", items.enum}, TitledMultiSelect{items.anyOf}, plus a LegacyTitledEnumSchema (the old enumNames).
  • Where. Simple enums in 2025-06-18; rich enums in 2025-11-25 and 2026-07-28.
  • mcpx (current, #2078). paramsFor (internal/mcpserver/conn.go) downgrades a titled single-select property to enum + enumNames for a pre-2025-11-25 host and strips default from every non-boolean property (the 2025-06-18 schema allows it only on booleans, ELI-05). A multi-select question has no 2025-06-18 equivalent and is refused whole for an old host (Question.Sendable): it stays with the broker rather than being flattened into a different answer type.
  • Value to mcpx. + low: downgrade titled single-select to enum + enumNames for 2025-06-18 hosts; multi-select has no 2025-06-18 equivalent and would have to be refused or flattened.
  • Effort. M — a schema rewrite per property, plus mapping multi-select answers back.
  • Risk. low: a strict 2025-06-18 host rejects the whole request, the same risk the conn.go comment names for mode.
  • Detail. Multi-select answers are string[] (ELI-06). LegacyTitledEnumSchema “will be removed in a future version” but is not in the 2026 deprecated-features registry.
  • Sources. schema/2025-06-18/schema.ts:1535 “enumNames?: string[]; // Display names for enum values”; schema/2025-11-25/schema.ts:2463 “export type EnumSchema =”; 2025-11-25/changelog.mdx:16 “Update”; schema/2026-07-28/schema.ts:3096; internal/mcpserver/conn.go:398-404.

ELI-05 default on every primitive schema

  • What. 2025-06-18 allows default only on BooleanSchema. 2025-11-25 (SEP-1034) adds it to string, number and enum schemas; clients that support defaults SHOULD prefill.
  • Where. 2025-11-25 and 2026-07-28. opencode v2 declares form: { applyDefaults: true }, so its SDK fills schema defaults into accepted content.
  • mcpx @ 05c78b2. Forwarded unchanged to hosts of every revision (as ELI-04). Whether mcpx’s own CLI prompt prefills was not examined.
  • Value to mcpx. + low: prefill in mcpx elicit prompts.
  • Effort. S.
  • Risk. low: an extra field to a strict 2025-06-18 host.
  • Detail. applyDefaults is an SDK option carried inside the form capability object, which the spec types as an open object.
  • Sources. schema/2025-06-18/schema.ts:1524 “default?: boolean;”; schema/2025-11-25/schema.ts:2247 “default?: string;”; 2025-11-25/client/elicitation.mdx:233 “All primitive types support optional default values”; v2:packages/core/src/mcp/client.ts:143.

ELI-06 ElicitResult.content values may be string[]

  • What. Accepted content values are string | number | boolean in 2025-06-18, and may also be string[] from 2025-11-25, for multi-select enums.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. On the server side answers stay raw JSON: a modern host’s inputResponses are read as map[string]json.RawMessage (internal/mcpserver/ask.go:79-89) and a legacy host’s reply is taken as raw (internal/mcpserver/ask.go:214). How the broker and CLI decode array answers was not examined.
  • Value to mcpx. + low: needed only once multi-select is rendered.
  • Effort. S.
  • Risk. low: a decoder typed to the 2025-06-18 shape rejects arrays.
  • Detail. Downgrading to a 2025-06-18 host would also have to map an array answer back (ELI-04).
  • Sources. schema/2025-06-18/schema.ts:1556 “content?: { [key: string]: string | number | boolean };”; schema/2025-11-25/schema.ts:2485 “content?: { [key: string]: string | number | boolean | string[] };”; internal/mcpserver/ask.go:82.

ELI-07 requestedSchema.$schema allowed

  • What. An optional $schema on the form schema (JSON Schema 2020-12 is the default dialect).
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Forwarded verbatim with the rest of the schema (ELI-02).
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. Not mentioned in the 2025-11-25 changelog.
  • Sources. schema/2025-11-25/schema.ts:2171 “$schema?: string;”.

ELI-08 mode field; omitted means form

  • What. mode?: "form" or mode: "url". Servers MAY omit it for form mode, and clients MUST treat an absent mode as form.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Stripped for hosts below 2025-11-25 (internal/mcpserver/conn.go:398-402); a URL question never reaches such a host because Sendable refuses it.
  • Value to mcpx. + low: done.
  • Effort. S.
  • Risk. None.
  • Detail. The same strip removes elicitationId (ELI-10).
  • Sources. schema/2025-11-25/schema.ts:2159 “mode?: "form";”; 2025-11-25/client/elicitation.mdx:97 “For backwards compatibility, servers”; internal/mcpserver/conn.go:398-402.

ELI-09 URL-mode elicitation

  • What. mode: "url", message, url: the user completes something out of band (credentials, OAuth, payment) and nothing but the URL passes through the client.
  • Where. 2025-11-25 and 2026-07-28. opencode v2 renders an external-link field and settles the form on the answer (or, in legacy, on the completion notification).
  • mcpx (current, #2079). Relays URL questions only to hosts that declared url (CAP-08). As a client it now declares url only with a handler installed (internal/mcpclient/modern.go capabilities; the pool installs the handler), and an upstream -32042 is opened through the broker’s existing elicitation/create path (internal/daemon/hooks.go, urlElicitationsViaBroker) before the call is retried once. The earlier state – accepting URL questions it never declared – is gone.
  • Value to mcpx. + med: declaring url would let OAuth-style servers use it legitimately rather than by accident.
  • Effort. S for the declaration; the per-host gating is CAP-08.
  • Risk. med: two upstreams choosing the same elicitationId collide in the broker, because the row id is theirs.
  • Detail. Clients MUST NOT prefetch the URL, MUST show it in full and MUST get consent before opening it. docs/protocol.md also lists as a known gap that mcpx never starts a URL elicitation of its own (for example for upstream OAuth). The 2026 URL params are only mode, message, url.
  • Sources. schema/2025-11-25/schema.ts:2189 “mode: "url";”; 2025-11-25/changelog.mdx:17 “Added support for”; internal/daemon/hooks.go:138 “ID: p.ElicitationID,”; internal/mcpclient/modern.go:51; docs/protocol.md:376 “url-mode elicitation raised by mcpx itself.”; schema/2026-07-28/schema.ts:2821-2838; v2:packages/core/src/mcp/index.ts:226.

ELI-10 elicitationId added, then removed

  • What. A required opaque id on 2025-11-25 URL requests, used to correlate the completion notification and the -32042 error. 2026 deletes it; a server encodes correlation in requestState instead.
  • Where. 2025-11-25 only.
  • mcpx @ 05c78b2. Stripped for hosts below 2025-11-25 (internal/mcpserver/conn.go:398-404). The 2026 path copies the upstream’s params verbatim (internal/mcpserver/ask.go:259), so a 2025-11-25 upstream’s elicitationId reaches a 2026 host whose schema has no such field. As a client, mcpx uses it as the broker row id (ELI-09).
  • Value to mcpx. + low: extend the strip to 2026 hosts.
  • Effort. S.
  • Risk. low: an undefined field sent to 2026 hosts (a send-conservatively conflict).
  • Detail. opencode v2’s code notes the same fact: “Legacy only: 2026-07-28 has no elicitationId and no completion notification”.
  • Sources. schema/2025-11-25/schema.ts:2200 “elicitationId: string;”; 2026-07-28/changelog.mdx:53 “Remove the”; internal/mcpserver/ask.go:259; internal/mcpserver/conn.go:398-404; v2:packages/core/src/mcp/index.ts:226 “Legacy only: 2026-07-28 has no elicitationId”.

ELI-11 notifications/elicitation/complete added, then removed

  • What. An optional server-to-client notice that an out-of-band URL interaction finished. Removed in 2026 because under MRTR the client learns the outcome by retrying.
  • Where. 2025-11-25 only. opencode v2 handles it (legacy URL mode); v1 does not.
  • mcpx @ 05c78b2. As a client it consumes it from upstreams (internal/mcpclient/client.go:501). As a server it has an emitter (internal/events/events.go:262-268) that nothing reaches: the listen filter has no elicitation kind (internal/cli/serve.go:727-739), so no host ever gets one and FeatElicitationComplete in the feature table is never exercised.
  • Value to mcpx. + low: a 2025-11-25 host that opened a URL through mcpx would learn completion early.
  • Effort. S — but it would have to be gated to 2025-11-25 hosts and to the one host that got the URL request.
  • Risk. low: MAY-level; hosts learn the outcome from the call’s result anyway.
  • Detail. The notification MUST go only to the client that received the URL request and MUST carry its elicitationId. The emitter maps any ElicitCompleted event, with no revision gate at that point; the feature table gives it a 2026 ceiling (internal/mcpserver/revisions.go:103).
  • Sources. schema/2025-11-25/schema.ts:2494 “notifications/elicitation/complete”; 2025-11-25/client/elicitation.mdx:398 “Servers MAY send a”; internal/mcpclient/client.go:501; internal/events/events.go:262-268; v2:packages/core/src/mcp/client.ts:160.

ELI-12 -32042 URLElicitationRequiredError added, then removed

  • What. 2025-11-25: a tool may fail with -32042 listing URL elicitations (data.elicitations) that must complete before a retry. 2026 removes the type; -32042 is reserved “2025-11-25 only” and MUST NOT be emitted. An InputRequiredResult carrying a URL elicitation/create replaces it.
  • Where. 2025-11-25 only.
  • mcpx (current, #2079 + #2080). As a client, -32042 is intercepted in CallTool at 2025-11-25 only (internal/mcpclient/client.go): the named elicitations open through the broker and the call retries exactly once. As a server, viaAsk (internal/mcpserver/url_required.go) now produces -32042 for a 2025-11-25 host that declared elicitation.url when a URL question of its call is pending and undeliverable (no channel at entry, or askClient returns ErrNoPush), listing exactly the pending URL questions via paramsFor; the broker keeps the questions. An upstream -32042 reaching the server side unhandled is relayed only to url-declared 2025-11-25 hosts, else the call fails generically. Never emitted at 2026 (the conformance gap elicitation-server-must-return-32042 is retired by #2080).
  • Value to mcpx. + med: as a client of 2025-11-25 upstreams, turn -32042 into URL questions for the host (legacy elicitation/create, or input_required for a 2026 host) and retry the call.
  • Effort. M.
  • Risk. med: tools that rely on -32042 fail through mcpx.
  • Detail. The constant is @internal and sits in the implementation-defined range. The removal is changelog-silent: the 2026 changelog mentions elicitationId and the notification but not this error; only basic/index.mdx and a schema comment record it. The 2025-11-25 changelog never named the addition either.
  • Also recorded from the errors register. Absent from internal/mcpserver and internal/mcpclient. As a client of 2025-11-25 upstreams, a tool that answers -32042 fails through mcpx instead of becoming a URL elicitation and a retry; as a server, an upstream -32042 would need translating for a 2026 host. The 2025-11-25 constant is marked @internal and commented as belonging to the [-32000, -32099] implementation-defined range; neither its addition nor its removal is in a changelog. Under the 2026 policy it lies in the specification’s sub-range, reserved as a historical code.
  • Sources. schema/2025-11-25/schema.ts:181 “export const URL_ELICITATION_REQUIRED = -32042;”; 2025-11-25/client/elicitation.mdx:426 “the server MAY return a”; 2026-07-28/basic/index.mdx:144 “URL elicitation required (2025-11-25 only).”.

ELI-13 Sensitive data: never → only via URL mode

  • What. 2025-06-18: servers MUST NOT request sensitive information by elicitation. 2025-11-25 narrows it to form mode and requires URL mode for sensitive interactions. 2026 keeps the 2025-11-25 rule.
  • Where. As stated.
  • mcpx @ 05c78b2. Relays whatever the upstream asks. Broker rows persist in SQLite (state/logs/elicit.db, internal/daemon/hooks.go:235-241), so a secret collected by a misbehaving upstream in form mode is stored.
  • Value to mcpx. + low: mcpx’s own prompts must never collect secrets by form.
  • Effort. S.
  • Risk. med: secrets in the broker database and the event bus if an upstream breaks the rule.
  • Detail. The rule binds the asking server, not the relay; a relay can only limit what it keeps.
  • Sources. 2025-06-18/client/elicitation.mdx:32 “Servers MUST NOT use elicitation to request sensitive information.”; 2025-11-25/client/elicitation.mdx:30 “Servers MUST NOT use form mode elicitation to request sensitive information such as”; internal/daemon/hooks.go:235-241.

ELI-14 2026 ElicitRequest embedded in InputRequiredResult, not a request

  • What. In 2026 ElicitRequest is a bare {method, params} (no jsonrpc, no id) inside inputRequests; its params have no _meta or task; ElicitResult no longer extends Result. A URL accept means consent; the outcome is learned on the retry.
  • Where. 2026-07-28. Elicitation is not deprecated there, unlike Roots, Sampling and Logging (SEP-2577 names only those three).
  • mcpx @ 05c78b2. Produces it for 2026 hosts (internal/mcpserver/ask.go:256-266) and answers it from modern upstreams with the same answer() used for legacy requests (internal/mcpclient/modern.go:103-135).
  • Value to mcpx. + med: done.
  • Effort. S.
  • Risk. None beyond the mechanism’s own gaps (mrtr.md).
  • Detail. A 2025-11-25 ElicitRequestFormParams extended TaskAugmentedRequestParams, so an elicitation could run as a client-hosted task; that path is gone (see tasks.md).
  • Sources. schema/2026-07-28/schema.ts:2856 “export interface ElicitRequest {”; schema/2026-07-28/schema.ts:3134 “export interface ElicitResult {”; 2026-07-28/client/elicitation.mdx:376 “The response with”; 2026-07-28/changelog.mdx:73; internal/mcpclient/modern.go:103-135.

ELI-15 Legacy delivery over Streamable HTTP: request on the POST’s SSE

  • What. A legacy server may send its own requests on the SSE stream of a POST before the response. 2026 forbids independent requests on any stream.
  • Where. 2025-03-26..2025-11-25 transport rule; elicitation itself from 2025-06-18; forbidden in 2026-07-28.
  • mcpx @ 05c78b2. When an upstream elicits during mcpx_call, prompts/get or resources/read, a ≥2025-06-18 host that declared elicitation and holds a session gets elicitation/create (negative id, -1) as an SSE frame on its POST; it answers with a separate POST (202), and the result follows on the same stream (internal/mcpserver/ask.go:212-229; internal/mcpserver/conn.go:228-274). Wire L1–L3 shows exactly this, with the message prefixed “ask (via mcpx) asks:”.
  • Value to mcpx. + high: inline answers from the host that made the call.
  • Effort. S — done.
  • Risk. low: it needs an Mcp-Session-Id, because the answer POST finds the waiting request by session (internal/mcpserver/server.go:1142-1151).
  • Detail. Only the three methods above are interruptible (internal/mcpserver/ask.go:318-325). A host’s call is held up to proto.askTimeout (10 m). When a relayed request times out, mcpx sends the host notifications/cancelled for it (see progress-cancellation.md). The transports register owns the SSE rules themselves.
  • Sources. 2025-03-26/basic/transports.mdx:111 “The server MAY send JSON-RPC requests and notifications before sending a”; 2026-07-28/basic/transports/streamable-http.mdx:117 “The server MUST NOT send independent JSON-RPC requests on this stream.”; internal/mcpserver/conn.go:395; internal/mcpserver/server.go:1142-1151; wire L1–L3.

ELI-16 Legacy delivery over stdio: request on the pipe

  • What. On stdio the server writes elicitation/create to stdout and reads the client’s reply from stdin while the original call is still open.
  • Where. 2025-06-18 and 2025-11-25 (2026 forbids server-written requests on stdio).
  • mcpx @ 05c78b2. An interruptible request runs on its own goroutine so the answer can arrive on the same read loop; replies are recognised by shape (an id and no method) and routed to the waiting request (internal/mcpserver/server.go:1032-1039, :1055-1062).
  • Value to mcpx. + high: works for every spawning host that declares elicitation.
  • Effort. S — done.
  • Risk. low: verified by reading code only; the wire study drove HTTP elicitation, not a stdio host answering.
  • Detail. Everything else on stdio is handled in line, so a long non-interruptible call still blocks the pipe (a transports-register item).
  • Sources. internal/mcpserver/server.go:1036 “if id, result, rerr, ok := replyOf([]byte(line)); ok {”; internal/mcpserver/server.go:1055-1062.

ELI-17 Stateless 2026 host plus eliciting upstream

  • What. A 2026 host that declares elicitation and calls, over HTTP without an Mcp-Session-Id (2026 removed it), an upstream tool that elicits. The host should receive an input_required result; from mcpx it gets -32603.
  • Where. 2026-07-28, server side.
  • mcpx @ 05c78b2. mint refuses an empty connection binding (internal/mcpserver/state.go:64), and an HTTP request without a session id gets an anonymous connection with an empty id (internal/mcpserver/server.go:1209-1218). In viaAsk the round counter is incremented before the mint and the failed mint continues without waiting, so rounds 1–8 spin at once and the ninth exceeds proto.askRounds; the call is abandoned (internal/mcpserver/ask.go:195-209). Wire M1: -32603 "tools/call was still asking for input after 8 rounds" after about a second. With a session id taken from server/discover the same call works (wire M2–M3).
  • Value to mcpx. + high: this is the whole 2026 server-side elicitation story for correct clients.
  • Effort. M — bind requestState to something the request carries (or to nothing but the HMAC, plus a method and params digest; see mrtr.md), and make a failed mint fall back to the broker as intended.
  • Risk. high: every stateless 2026 host that declares elicitation gets an internal error from any eliciting upstream. A 2026 host that declares nothing takes the broker path and works.
  • Detail. Both the code comment and the doc say the intended fallback is the broker (“the question goes back to the broker”; “its questions stay with the broker”); the round counter defeats it. The binding choice itself is registered in mrtr.md; that mcpx mints a session id on server/discover at all is a lifecycle-register item.
  • Sources. internal/mcpserver/state.go:64; internal/mcpserver/ask.go:195-209; internal/mcpserver/ask.go:204 “No verifiable state means no safe resume”; docs/protocol.md:225-228 “is issued no token at all, and its questions stay with the broker”; internal/mcpserver/server.go:1209-1218; 2026-07-28/changelog.mdx:12 “Remove protocol-level sessions”; wire M1, M2–M3.

ELI-18 Naming the originating server in a relayed question

  • What. Clients MUST make clear which server is asking. Behind a proxy the client sees only the proxy, so the proxy has to put the originator into the question.
  • Where. Client rule from the elicitation pages; mcpx applies it on the legacy path only.
  • mcpx @ 05c78b2. Legacy: paramsFor prefixes "<server> (via mcpx) asks: " unless the message already names the server (internal/mcpserver/conn.go:390-396; wire L2). 2026: inputRequired copies the upstream’s params verbatim, with no prefix (internal/mcpserver/ask.go:259; wire M2 shows the bare “Which repository should this go in?”).
  • Value to mcpx. + med: a 2026 host cannot tell which upstream is asking, which matters most for confirmations and credentials.
  • Effort. S — run the same rewrite on the modern path (and drop elicitationId there, ELI-10).
  • Risk. med: users answering questions without knowing who asked.
  • Detail. The inputRequests keys are broker ids (elc-…), which is fine; only the params differ between the paths.
  • Sources. 2025-11-25/client/elicitation.mdx:42 “Provide UI that makes it clear which server is requesting information”; internal/mcpserver/conn.go:395; internal/mcpserver/ask.go:259 “requests[q.ID] = map[string]any{”; wire L2, M2.

ELI-19 Question from a shared upstream instance

  • What. A question arrives on an upstream connection, not on a call. When two calls share one pooled instance (sharing: shared), mcpx cannot tell which call asked.
  • Where. mcpx design; the spec is silent.
  • mcpx @ 05c78b2. forKey attributes a question only when exactly one call is running on that (server, scope key) (internal/daemon/routes_proto.go:151-160); otherwise the question goes to the broker’s default audience. The doc says so and why (docs/protocol.md:241-244).
  • Value to mcpx. + low: safety over convenience; one host’s credential prompt never reaches another host.
  • Effort. L — needs request-level correlation from the upstream.
  • Risk. low: hosts using shared servers never get inline questions.
  • Detail. 2025-11-25’s io.modelcontextprotocol/related-task _meta would correlate exactly for task-augmented upstream calls, but mcpx never task-augments upstream calls (see tasks.md) and 2026 drops the key.
  • Sources. docs/protocol.md:241-244 “mcpx makes none, and the question goes to the broker”; internal/daemon/routes_proto.go:156.

ELI-20 Questions raised inside mcpx_exec

  • What. Upstream calls made by a script never interrupt the MCP host that ran the script; their questions go to the broker.
  • Where. mcpx design.
  • mcpx @ 05c78b2. daemonAsker.Begin treats only mcpx_call as interruptible (internal/cli/serve_ask.go:44-58); the gap is written down (docs/protocol.md:379-382): the correlation identifies a call, and a script is many calls.
  • Value to mcpx. + low: scripts that elicit could run under the host’s UI.
  • Effort. L.
  • Risk. low: nothing new breaks.
  • Detail. The script-side answer to the same problem is ELI-21.
  • Also recorded from the code mode register. opencode v2 (UI form, attributed to a "global" sentinel session because the server “cannot attribute them to a persisted session row”). Cloudflare (agent-level handlers). mcpx (broker; no script hook). opencode v1 (capability commented out; answers -32601) and lootbox (capabilities: {}; elicitation register, ELI-22) refuse. Broker routes GET /v1/elicit, GET /v1/elicit/{id}, POST /v1/elicit/{id}/{action} (internal/daemon/server.go:422-424). daemonAsker.Begin treats only mcpx_call as interruptible (internal/cli/serve_ask.go:44-58). The script-side onElicit(q => …) is designed in a proposal (docs/elicitation.md:7 status proposed; docs/elicitation.md:308-330) and not built (rg finds no onElicit in internal/codegen). mcpx defers the inline case on purpose: “A script makes many upstream calls and the question belongs to one of them; the correlation in §3.4 identifies a call, and a script is not one” (docs/protocol.md:379-382). Protocol details live in the elicitation register (ELI-20, ELI-21).
  • Sources. docs/protocol.md:379 “A modern client cannot answer a question raised by”; internal/cli/serve_ask.go:57 “p.Name != "mcpx_call"”.; docs/elicitation.md:321 onElicit(async (q) => {; v2:packages/core/src/mcp/client.ts:143; v2:packages/core/src/mcp/index.ts:80-82; https://developers.cloudflare.com/agents/model-context-protocol/apis/client-api/#elicitation (“On the stateless path, elicitation returns input_required and completes through multi-round-trip requests (MRTR).”)

ELI-21 Answering an elicitation from inside a script

  • What. When a tool call made by a script asks a question, the script itself could answer it (it often knows the repository or the choice). opencode v2 shows a form in its UI instead; Cloudflare’s agent runs registered handlers.
  • Where. opencode v2 (UI form, form and URL modes). mcpx: daemon broker only. opencode v1 and lootbox declare no elicitation capability.
  • mcpx @ 05c78b2. The broker has GET /v1/elicit, GET /v1/elicit/{id} and POST /v1/elicit/{id}/{action} (internal/daemon/server.go:422-424), but the generated script client has no onElicit; rg finds none in internal/codegen. docs/elicitation.md:308-330 describes onElicit as if a script could register it today.
  • Value to mcpx. + med: a script that expects a question could answer from its own data instead of failing or waiting for a human.
  • Effort. M — a long-poll or SSE from the generated client to /v1/elicit?run=<id>, plus handler registration.
  • Risk. low: without it scripts depend on policy answers or a human at the CLI or TUI.
  • Detail. The doc is ahead of the code, so the gap is a doc/code disagreement as well as a missing feature.
  • Sources. docs/elicitation.md:318-324 “onElicit(async (q) => {”; internal/daemon/server.go:422-424; v2:packages/core/src/mcp/client.ts:143.

ELI-22 Client with no elicitation answerer

  • What. What a client replies when it receives elicitation/create and has nothing to answer with.
  • Where. opencode v1 registers only roots/list, so its SDK answers every other server request with -32601. lootbox declares capabilities: {} and registers no handler, so its SDK does the same. mcpx declares elicitation and replies cancel.
  • mcpx @ 05c78b2. As a server it never sends elicitation/create to a host that did not declare it (internal/mcpserver/conn.go:192), so opencode v1 never sees one. As a client it answers cancel rather than an error (internal/mcpclient/modern.go:129-131).
  • Value to mcpx. + low: confirms that capability gating is enough.
  • Effort. S.
  • Risk. low. docs/opencode-plugin.md:184 says v1 “will get cancel”; on the wire v1 would answer -32601, not an ElicitResult (moot, because mcpx never sends it).
  • Detail. The 1.x SDK also refuses to register an elicitation handler without the capability.
  • Sources. v1:packages/opencode/src/mcp/index.ts:77; v1:packages/opencode/src/mcp/index.ts:44 “// elicitation: {},”; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153; internal/mcpclient/modern.go:129-131; docs/opencode-plugin.md:184.

ELI-23 Elicitation attributed to a session or a whole location

  • What. opencode v2 turns elicitation/create into a form owned by the sentinel session id "global", because “the server cannot attribute them to a persisted session row”. Every session in that Location sees it.
  • Where. opencode v2 only (v1 has no elicitation).
  • mcpx @ 05c78b2. mcpx is the server here: its questions reach v2 users as Location-global forms. On the legacy path the “(via mcpx) asks:” prefix at least names the upstream (ELI-18).
  • Value to mcpx. + low: v2 users can answer mcpx questions at all.
  • Effort. S.
  • Risk. low: with several sessions open, it is unclear whose question it is.
  • Detail. v2 maps form properties to typed fields and URL mode to an external-link field. mcpx’s own analogue of the attribution problem is ELI-19.
  • Sources. v2:packages/core/src/mcp/index.ts:80 “MCP elicitations are Location-scoped, not Session-scoped”; v2:packages/core/src/mcp/index.ts:82; v2:packages/core/src/mcp/client.ts:160.

Errors

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  JSON-RPC and MCP error codes across revisions, their HTTP statuses, tool errors versus protocol errors, and what mcpx sends.

MCP reports failures two ways: JSON-RPC error responses (protocol errors) and, for tools, successful results with isError: true (execution errors). The code set barely moved until 2026-07-28, which added an allocation policy, three codes (-32020..-32022) that must travel with HTTP 400, moved resource-not-found to -32602, and gave unknown methods HTTP 404. For mcpx, a dual-era proxy, the two things that matter most are choosing codes by the peer’s revision (it sends the 2026-only -32022 to legacy clients and -32602 for a missing resource to everyone) and the HTTP status (every JSON-RPC error goes out as 200, which defeats the status-based era detection 2026 clients use). mcpx answering unknown notifications with an error is in the notifications register.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
ERR-01-32700 Parse error2024-11-05 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (typed)✓ — sent without id; HTTP 400+ lowSlow
ERR-02-32600 Invalid Request2024-11-05 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (typed)partial — stdio checks jsonrpc; HTTP never does+ lowSlow
ERR-03-32601 Method not found, and what it now covers2024-11-05 has 2026-07-28 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ + 404partial — used for unknown methods; always HTTP 200+ medSlow
ERR-04-32602 Invalid params, meanings widening2024-11-05 has 2026-07-28 has mcpx missing24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (wider)partial — also used for internal failures (#202)+ medSmed
ERR-05-32603 Internal error2024-11-05 has 2026-07-28 has mcpx missing24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — MRTR path uses it; resources and prompts use -32602 (#202)+ medSlow
ERR-06-32002 Resource not found → -326022024-11-05 has 2026-07-28 removes mcpx missing 2026-07-28 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 MUST NOT emitpartial — -32602 to every era; legacy expects -32002 (#202)+ lowSlow
ERR-07-32020 HeaderMismatch2026-07-28 has mcpx missing26-07 ✓ (HTTP 400)✗ — plain-JSON 400 for one case; others unchecked (#199)+ highShigh
ERR-08-32021 MissingRequiredClientCapability2026-07-28 has26-07 ✓ (HTTP 400)✗ — never used; the broker answers instead+ lowSlow
ERR-09-32022 UnsupportedProtocolVersion2026-07-28 has mcpx missing26-07 ✓ (HTTP 400)partial — right shape; requested empty; HTTP 200 (#199)+ medSmed
ERR-10Code for refusing a legacy initialize: -32602 vs -320222024-11-05 has specs conflict mcpx missing24-11..25-11 example -32602 · 26-07 -32022✗ — 2026-only -32022 sent to legacy peers (#202)+ medSmed
ERR-11Draft codes -32001 / -32003 / -320042026-07-28 haspre-release 2026 drafts onlyn/a — never used by mcpx+ lowSlow
ERR-12Error-code allocation policy (2026)2026-07-28 has specs conflict26-07 ✓✓ — standard codes plus -32022 only+ lowSlow
ERR-13HTTP status for modern JSON-RPC errors (400 / 404)2026-07-28 has mcpx missing26-07 ✓✗ — every error goes out as 200 (#199)+ medSmed
ERR-14Check order for a malformed 2026 HTTP request2026-07-28 has specs conflict26-07 (unspecified)partial — header-vs-_meta first; missing fields never rejected+ lowSlow
ERR-15Error response id optional2025-11-25 has24-11 — · 25-03 — · 25-06 — · 25-11 ✓ · 26-07 ✓partial — sends id-less parse errors; client drops id-less errors+ lowSlow
ERR-16Local errors versus peer errors through a proxy2026-07-28 has mcpx missing26-07 guidancepartial — upstream code and data survive only as text (#202)+ medMmed
ERR-17Tool input validation: protocol error → isError2025-11-25 has24-11..25-06 protocol error · 25-11 ✓ · 26-07 ✓ isError✓ — tool failures are isError results+ medSlow
ERR-182026 schema still shows -32602 for bad tool arguments2026-07-28 has specs conflict26-07 prose vs schema✓ — mcpx follows the prose+ lowSlow

ERR-01 -32700 Parse error

  • What. Invalid JSON received. Standard JSON-RPC; 2026 adds a typed ParseError interface.
  • Where. Every revision.
  • mcpx @ 05c78b2. stdio answers -32700 with no id (internal/mcpserver/server.go:1040-1044); HTTP answers 400 with -32700 (internal/mcpserver/server.go:1153-1157). Batch arrays land here (wire W16, S3; transports.md TR-09), and so does an HTTP body over 64 MiB, which is truncated before parsing (internal/mcpserver/server.go:1132).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. Legacy HTTP allows an error status whose body MAY be an id-less JSON-RPC error, which is what mcpx sends. The mcpx client drops id-less errors it receives (ERR-15).
  • Sources. schema/2024-11-05/schema.ts:80 “export const PARSE_ERROR = -32700;”; schema/2026-07-28/schema.ts:328; internal/mcpserver/server.go:1042-1043; wire S3.

ERR-02 -32600 Invalid Request

  • What. The message is not a valid request object; the 2026 doc names missing required fields such as jsonrpc or method, or wrong types for them. 2025-11-25 also uses it for misuse of task-required execution.
  • Where. Every revision.
  • mcpx @ 05c78b2. stdio answers -32600 when jsonrpc is present and not "2.0", but accepts a frame with no jsonrpc at all (internal/mcpserver/server.go:1046-1049). The HTTP path has no such check; line 1046 is the only one.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low; accepting a missing jsonrpc is accept-liberally.
  • Detail. None.
  • Sources. schema/2024-11-05/schema.ts:81; schema/2026-07-28/schema.ts:339; 2025-11-25/basic/utilities/tasks.mdx:775; internal/mcpserver/server.go:1046 “if req.JSONRPC != "" && req.JSONRPC != "2.0" {”.

ERR-03 -32601 Method not found, and what it now covers

  • What. Unknown method, in every revision. It accumulates meanings: from 2025-03-26 the completion page uses it for completion/complete without the capability; 2025-11-25 for a taskSupport: "required" tool called without a task; the 2026 schema for any method “gated behind a server capability the server did not advertise” (a missing client capability is -32021 instead); and 2026 HTTP pairs it with status 404. The tasks extension also requires it for the removed tasks/result.
  • Where. Every revision; 404 pairing in 2026-07-28.
  • mcpx @ 05c78b2. Unrecognised methods fall through to fail(-32601, "no method …") (internal/mcpserver/server.go:766), sent with HTTP 200 (wire W12; ERR-13). The same fallthrough answers unknown notifications, which is covered in the notifications register.
  • Value to mcpx. + med: the 404 lets 2026 clients tell “unknown method” from “old server”.
  • Effort. S.
  • Risk. Low.
  • Detail. None.
  • Sources. schema/2026-07-28/schema.ts:346 “In MCP, a server returns this error when a client invokes a method the server does not implement”; 2025-03-26/server/utilities/completion.mdx:130; 2026-07-28/basic/transports/streamable-http.mdx:271; internal/mcpserver/server.go:766; wire W12.

ERR-04 -32602 Invalid params, meanings widening

  • What. Invalid or malformed parameters. The documented uses grow: unknown tool or prompt, missing prompt arguments, invalid or expired cursor, invalid log level, the legacy initialize version example (ERR-10), an elicitation mode the client did not declare, malformed sampling tool results; 2026 adds a request missing required _meta fields (HTTP 400) and a missing resource (ERR-06).
  • Where. Every revision; widest in 2026-07-28.
  • mcpx @ 05c78b2. tools/call params that do not decode get -32602 (internal/mcpserver/server.go:662-664). Every resources/read and prompts/get failure also gets it, internal ones included (ERR-05), and a missing resource gets it in every era (ERR-06). A request missing required _meta is not rejected at all (lifecycle-versioning.md LV-24).
  • Value to mcpx. + med: meaning “fix your parameters”, it should not be used for server faults.
  • Effort. S.
  • Risk. Clients retry with “fixed” parameters against a broken server.
  • Detail. In 2026 an invalid per-request logLevel SHOULD reject the whole request with -32602; see the logging register. The elicitation and sampling cases are errors a client returns, and in 2026 a client answers those requests only inside MRTR, where InputResponse has no error variant, so the documented -32602 has no channel (MRTR register).
  • Also recorded from the resources register. JSON-RPC semantics generally. A daemon or upstream failure (process crash, timeout, unknown namespace) in resources/read or prompts/get comes back as -32602 (internal/mcpserver/server.go:714-716, internal/mcpserver/server.go:755-758). The MRTR path uses -32603 for the same failures (internal/mcpserver/ask.go:147-149), so the two paths disagree. Distinguishing “no such resource” (-32602) from “upstream down” (-32603) needs the backend to return a typed error. Overlaps the errors register.
  • Sources. schema/2026-07-28/schema.ts:389; schema/2026-07-28/schema.ts:366; schema/2026-07-28/schema.ts:370-371; schema/2026-07-28/schema.ts:541-542; 2026-07-28/basic/index.mdx:381; 2024-11-05/server/utilities/logging.mdx:110; internal/mcpserver/server.go:715.; 2026-07-28/server/resources.mdx:405 “Servers SHOULD return -32603 for internal errors.”; internal/mcpserver/server.go:755-758; internal/mcpserver/ask.go:147-149

ERR-05 -32603 Internal error

  • What. The receiver hit an unexpected condition. 2026’s resources page says servers SHOULD use -32603 for internal errors.
  • Where. Every revision.
  • mcpx @ 05c78b2. A daemon or upstream failure during resources/read or prompts/get (process crash, timeout, unknown namespace) is reported as -32602 (internal/mcpserver/server.go:714-716, :755-758). The MRTR path reports the same failures as -32603 (internal/mcpserver/ask.go:147-149, :170-172), so the two paths disagree. The -32603 a stateless 2026 client gets when MRTR gives up is in the elicitation register.
  • Value to mcpx. + med: clients stop retrying corrected parameters against a broken server.
  • Effort. S.
  • Risk. Low.
  • Detail. None.
  • Sources. schema/2024-11-05/schema.ts:84; schema/2026-07-28/schema.ts:403; 2026-07-28/server/resources.mdx:405 “Servers SHOULD return -32603 for internal errors.”; internal/mcpserver/server.go:714-716; internal/mcpserver/ask.go:147-149.

ERR-06 -32002 Resource not found → -32602

  • What. 2024-11-05..2025-11-25 answer a missing resource with -32002. 2026 servers MUST use -32602 and MUST NOT emit -32002; clients SHOULD still accept -32002; a missing resource MUST NOT be answered with empty contents.
  • Where. -32002 through 2025-11-25; -32602 in 2026-07-28.
  • mcpx @ 05c78b2. Any backend error becomes -32602 for every era (internal/mcpserver/server.go:713-716). Wire W8: {"code":-32602,"message":"mcp error -32602: no such resource: nope"}; the upstream’s own code survives only as text in the message and its data is lost. -32002 appears nowhere in internal/mcpserver or internal/mcpclient.
  • Value to mcpx. + low: legacy hosts that special-case -32002 show “not found” properly; as a proxy mcpx should translate in both directions by peer revision.
  • Effort. S — choose the code by peer.Version.
  • Risk. Low.
  • Detail. -32002 sits in the legacy implementation-defined band; it is the one code there that 2026 lets receivers interpret, and it is reserved, never reused.
  • Also recorded from the resources register. -32002 in 2024-11-05 through 2025-11-25 (SHOULD); Any backend error from a read becomes -32602 in every era (internal/mcpserver/server.go:713-716); wire W8: {"code":-32602,"message":"mcp error -32602: no such resource: nope"}. Correct for 2026; a legacy host that special-cases -32002 does not recognise it. The upstream’s own code survives only as text inside mcpx’s message; its data is lost. -32002 sat in JSON-RPC’s implementation-defined band; 2026 declares -32000..-32019 legacy/implementation-defined with -32002 the only one receivers may interpret. Overlaps the errors register.
  • Sources. 2025-11-25/server/resources.mdx:388 “- Resource not found: -32002”; 2026-07-28/server/resources.mdx:404; 2026-07-28/server/resources.mdx:407; 2026-07-28/basic/index.mdx:140; internal/mcpserver/server.go:715; wire W8.; 2024-11-05/server/resources.mdx:337 “- Resource not found: -32002”; internal/mcpserver/server.go:713-716

ERR-07 -32020 HeaderMismatch

  • What. A server that processes the body MUST reject a request whose required standard header (MCP-Protocol-Version, Mcp-Method, Mcp-Name) is missing, disagrees with the body, or contains invalid characters, with HTTP 400 and a JSON-RPC -32020 error; values using the Base64 sentinel are decoded before comparing. Intermediaries must return an HTTP error but need not use JSON-RPC.
  • Where. 2026-07-28 (numbered -32001 in the pre-release draft).
  • mcpx @ 05c78b2. Only a mismatch between MCP-Protocol-Version and _meta is caught, and it is answered 400 with {"error": "MCP-Protocol-Version … does not match …"}, not JSON-RPC (internal/mcpserver/server.go:1159-1168; wire W10). Mcp-Method and Mcp-Name are never checked (wire W11), a 2026 request with no version header is not rejected, and -32020 appears nowhere in internal/. As a client, a -32020 from an upstream is not recognised as modern (lifecycle-versioning.md LV-28).
  • Value to mcpx. + high: a dual-era client decides “modern server” from a recognised JSON-RPC error in a 400 body; mcpx’s plain body makes it fall back to initialize, which mcpx answers, so the exchange works but in the wrong era.
  • Effort. S.
  • Risk. Dual-era clients misclassify mcpx; a policy proxy trusting the headers can be fooled.
  • Detail. Apply it only to modern-era requests; legacy requests carry no Mcp-Method. The schema’s own note on _meta protocolVersion says a mismatch returns “400 Bad Request” without naming a code, while the transport page requires -32020. A client seeing -32020 for an x-mcp-header tool SHOULD re-list tools and retry (transports.md TR-39).
  • Sources. schema/2026-07-28/schema.ts:434 “export const HEADER_MISMATCH = -32020;”; 2026-07-28/basic/transports/streamable-http.mdx:597 “When rejecting a request due to header validation failure, servers MUST”; 2026-07-28/basic/transports/streamable-http.mdx:624; schema/2026-07-28/schema.ts:71-72; internal/mcpserver/server.go:1163; wire W10.

ERR-08 -32021 MissingRequiredClientCapability

  • What. If processing needs a capability absent from this request’s clientCapabilities, the server MUST return -32021 with data.requiredCapabilities, a full ClientCapabilities object; HTTP 400.
  • Where. 2026-07-28 (numbered -32003 in the pre-release draft).
  • mcpx @ 05c78b2. Never used, and no constant exists. When a 2026 client did not declare elicitation, canAsk is false and the question goes to mcpx’s broker, where another audience can answer it (internal/mcpserver/ask.go:66-76; internal/mcpserver/server.go:649-657).
  • Value to mcpx. + low: a host that could re-send with the capability never learns it was needed; the broker is a deliberate alternative.
  • Effort. S.
  • Risk. Low; not using it is conformant while mcpx can finish without the capability.
  • Detail. MRTR forbids input requests for undeclared capabilities, so when input is truly required this is the only compliant answer (MRTR register). The tasks extension uses it for clients that did not declare the extension (tasks register).
  • Also recorded from the elicitation register. 2026-07-28 only (earlier drafts numbered it -32003). Never emitted, and no constant exists. A 2026 host that did not declare elicitation gets the direct path: canAsk is false (internal/mcpserver/ask.go:66-76) and the question waits in the broker for another audience. The findings disagree on the label: the base-protocol axis calls -32021 the only compliant answer when input is truly required; the mcpx axis calls the broker fallback a design choice. Both hold: mcpx never requires the capability, because a broker audience can answer. data.requiredCapabilities is a full ClientCapabilities object. The rule that mcpx never puts an undeclared kind of question into inputRequests is in mrtr.md.
  • Sources. schema/2026-07-28/schema.ts:442 “export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021;”; schema/2026-07-28/schema.ts:523 “requiredCapabilities: ClientCapabilities;”; 2026-07-28/basic/index.mdx:387; internal/mcpserver/ask.go:67.; schema/2026-07-28/examples/MissingRequiredClientCapabilityError/missing-elicitation-capability.json:6

ERR-09 -32022 UnsupportedProtocolVersion

  • What. If the requested version is unknown, or known but deliberately unsupported (an experimental draft, say), the server MUST return -32022 with data.supported: string[] and data.requested: string; the client SHOULD retry with a mutually supported version. HTTP 400.
  • Where. 2026-07-28 (numbered -32004 in the pre-release draft).
  • mcpx @ 05c78b2. The pre-dispatch check (internal/mcpserver/server.go:469-471) calls unsupportedVersion, which reads params.protocolVersion, the initialize field, instead of _meta (internal/mcpserver/server.go:245-248), so requested is always "" on modern requests. It goes out with 200 (ERR-13). Wire W13: HTTP/1.1 200 OK … "data":{"requested":"","supported":[…]}. The supported list is right and includes 2026-07-28.
  • Value to mcpx. + med: a correct error lets a modern client pick a version and retry, and the 400 is how dual-era clients recognise a modern server.
  • Effort. S — read the _meta key; map the status.
  • Risk. Clients that inspect requested or key on the 400 misbehave.
  • Detail. mcpx’s own client recognises modern servers by this code alone, via a substring test (lifecycle-versioning.md LV-28).
  • Sources. 2026-07-28/basic/versioning.mdx:50 “support), it MUST respond with an”; schema/2026-07-28/schema.ts:483 “export interface UnsupportedProtocolVersionError extends Omit<”; 2026-07-28/basic/transports/streamable-http.mdx:265; internal/mcpserver/server.go:248; wire W13.

ERR-10 Code for refusing a legacy initialize: -32602 vs -32022

  • What. Every legacy lifecycle page shows an “initialization error” example: -32602 "Unsupported protocol version" with data.supported and data.requested, the same data shape 2026 standardises under -32022. mcpx answers legacy initialize refusals with -32022, a code no legacy revision defines.
  • Where. Example in 2024-11-05..2025-11-25; -32022 in 2026-07-28 only.
  • mcpx @ 05c78b2. codeUnsupportedVersion = -32022 (internal/mcpserver/server.go:230) is used for every initialize refusal (internal/mcpserver/server.go:506-509); wire W1, W3, W4, S1.
  • Value to mcpx. + med. A dual-era client may read -32022 as “modern server”, which happens to reach mcpx’s discover path; a legacy client sees an unknown code. For any well-formed version the legacy answer is a counter-offer, not an error (lifecycle-versioning.md LV-05..LV-07); an error remains right only for garbage, where the legacy example uses -32602.
  • Effort. S.
  • Risk. Legacy clients that switch on -32602 treat the refusal as unknown; mcpx’s own client aborts on it (lifecycle-versioning.md LV-29).
  • Detail. The legacy example requests "1.0.0", not a date. 2026’s versioning page tells a modern-only server to name its versions in any error to initialize without assigning a code. Sending a 2026 code to a legacy peer is a send-conservatively conflict.
  • Sources. 2024-11-05/basic/lifecycle.mdx:210 “"code": -32602,”; 2025-11-25/basic/lifecycle.mdx:278; 2026-07-28/basic/versioning.mdx:59 “"code": -32022,”; schema/2026-07-28/schema.ts:450; internal/mcpserver/server.go:230; wire W1.

ERR-11 Draft codes -32001 / -32003 / -32004

  • What. Before 2026-07-28 was released its draft numbered HeaderMismatch -32001, MissingRequiredClientCapability -32003 and UnsupportedProtocolVersion -32004; they were renumbered to -32020..-32022. Pre-release SDKs may still send the old numbers, which now fall in the band where receivers must assume nothing.
  • Where. Pre-release 2026 drafts only.
  • mcpx @ 05c78b2. Never used. A pre-release server sending -32004 would not be recognised as modern by mcpx’s -32022 substring test (internal/mcpclient/client.go:330), which is also what the policy requires.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. None.
  • Sources. 2026-07-28/changelog.mdx:65 “HeaderMismatch -32001 → -32020, MissingRequiredClientCapability”; internal/mcpclient/client.go:330.

ERR-12 Error-code allocation policy (2026)

  • What. JSON-RPC’s -32000..-32099 is split. -32000..-32019 is legacy: no new allocations, new implementations SHOULD NOT use it, and receivers MUST NOT assume a meaning, except for -32002. -32020..-32099 belongs to the specification: implementations MUST NOT emit undefined codes there and MUST use defined ones only with their meaning; codes are allocated sequentially from -32020, and earlier codes (-32002, -32042) stay reserved. New application errors SHOULD be allocated outside -32768..-32000.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Uses only the JSON-RPC standard codes and -32022 (internal/mcpserver/server.go:222-231).
  • Value to mcpx. + low: nothing to change today; it constrains any code mcpx adds.
  • Effort. S.
  • Risk. None.
  • Detail. The schema’s comment describes -32000..-32019 as implementation-defined, used by existing SDKs “for their own purposes”, without the prose’s SHOULD NOT for new implementations.
  • Sources. 2026-07-28/basic/index.mdx:117 “- -32000 to -32019 — legacy. Codes in this sub-range were allocated by”; 2026-07-28/basic/index.mdx:122; 2026-07-28/basic/index.mdx:153-154; schema/2026-07-28/schema.ts:413-416; internal/mcpserver/server.go:222-231.

ERR-13 HTTP status for modern JSON-RPC errors (400 / 404)

  • What. For 2026 requests over HTTP: -32602 for missing _meta, -32020, -32021 and -32022 MUST go out with 400; an unimplemented method MUST go out as 404 with -32601, whose body distinguishes it from an old HTTP+SSE server’s bare 404. Legacy Streamable HTTP ties no JSON-RPC code to a status: its 400 is for a bad version header or a missing required session, and its 404 for an expired session.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Every non-streaming response, errors included, is written with 200 (internal/mcpserver/server.go:1192). Wire W12 (-32601, 200), W13 (-32022, 200), W8 (-32602, 200).
  • Value to mcpx. + med: dual-era clients classify servers by these statuses (lifecycle-versioning.md LV-30, transports.md TR-11).
  • Effort. S — map by code when the request is modern; keep 200 for legacy requests.
  • Risk. A dual-era client probing mcpx with a 2026 request gets a 200 error and may misclassify it.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:265 “not to support), it MUST respond with 400 Bad Request and an”; 2026-07-28/basic/transports/streamable-http.mdx:271 “If the server does not implement the requested RPC method, it MUST respond”; 2026-07-28/basic/index.mdx:381; internal/mcpserver/server.go:1192 “writeJSON(w, http.StatusOK, resp)”; wire W12.

ERR-14 Check order for a malformed 2026 HTTP request

  • What. A missing MCP-Protocol-Version header is a HeaderMismatch (-32020); a missing _meta protocolVersion is -32602; an unsupported version is -32022. All three are 400, and the spec does not order the checks, for example when the header is present with an unsupported version and _meta is absent. Only the JSON-RPC code differs, and dual-era clients classify by that code.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Order: parse (-32700), then header against _meta only when both are present (plain 400, internal/mcpserver/server.go:1161), then session, then the version check before dispatch (-32022, internal/mcpserver/server.go:469); missing header or _meta fields are never rejected.
  • Value to mcpx. + low: pick one order and document it.
  • Effort. S.
  • Risk. Clients may classify the same malformed request differently across servers.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:624 “- A required standard header (MCP-Protocol-Version, Mcp-Method,”; 2026-07-28/basic/index.mdx:380-381; internal/mcpserver/server.go:1161; internal/mcpserver/server.go:469.

ERR-15 Error response id optional

  • What. 2024-11-05..2025-06-18 schemas require id on JSONRPCError; 2025-11-25 makes it optional for errors where the id could not be read, and splits JSONRPCResultResponse from JSONRPCErrorResponse. Earlier HTTP prose already allowed id-less error bodies on 4xx.
  • Where. 2025-11-25, 2026-07-28.
  • mcpx @ 05c78b2. The server sends parse errors without an id (internal/mcpserver/server.go:1042-1043, :1155-1156). The client skips any frame without an id as a notification (internal/mcpclient/client.go:369-370), so an id-less error from an upstream, such as its parse error for one of mcpx’s frames, is dropped and the pending call waits out its timeout.
  • Value to mcpx. + low.
  • Effort. S — fail the in-flight call, or log, on an id-less error.
  • Risk. A strict 2025-06-18 client may reject id-less errors; that is unavoidable for parse errors.
  • Detail. None.
  • Sources. schema/2024-11-05/schema.ts:91 “id: RequestId;”; schema/2025-11-25/schema.ts:161 “id?: RequestId;”; 2025-11-25/basic/index.mdx:89; internal/mcpclient/client.go:369-370.

ERR-16 Local errors versus peer errors through a proxy

  • What. 2026: errors purely local to an implementation, such as an SDK timeout, have no codes, and implementations that surface them in JSON-RPC shape should make sure they cannot be mistaken for errors from the peer. A proxy has three sources to keep apart: the upstream’s error, its own failures, and local timeouts.
  • Where. 2026-07-28 guidance.
  • mcpx @ 05c78b2. An upstream error arrives as Go error text (mcp error -32602: no such resource: nope) and is re-wrapped in a fresh -32602 (internal/mcpserver/server.go:713-716; wire W8), so the upstream’s data is lost and a daemon or transport failure looks like an upstream invalid-params error (ERR-05). Whether other upstream codes, such as -32002 or -32042, are ever forwarded unchanged was not verified.
  • Value to mcpx. + med: an agent can tell “the upstream said no” from “mcpx broke”.
  • Effort. M — carry upstream code and data through the daemon, and mark local failures.
  • Risk. Agents retry the wrong thing.
  • Detail. None.
  • Sources. 2026-07-28/basic/index.mdx:146 “Errors that are purely local to an implementation (for example, a request”; 2026-07-28/basic/index.mdx:149; internal/mcpserver/server.go:715; wire W8.

ERR-17 Tool input validation: protocol error → isError

  • What. 2024-11-05..2025-06-18 list unknown tools and invalid arguments as protocol errors and execution failures as isError: true results. 2025-11-25 (SEP-1303) moves input-validation failures into tool execution errors so the model can correct itself; protocol errors are left for unknown tools, malformed requests and server errors, and clients SHOULD pass execution errors to the model.
  • Where. 2025-11-25 and 2026-07-28 prose.
  • mcpx @ 05c78b2. A failing tool is an isError: true result (internal/mcpserver/server.go:666-674); params that do not decode get -32602 (internal/mcpserver/server.go:662-664). In the proxy direction mcpx_call loses an upstream’s isError (tools register).
  • Value to mcpx. + med: matches mcpx’s design; as a proxy, pass upstream errors through unchanged.
  • Effort. S — done on the server side.
  • Risk. Low.
  • Detail. None.
  • Sources. 2024-11-05/server/tools.mdx:231 “1. Protocol Errors: Standard JSON-RPC errors for issues like:”; 2024-11-05/server/tools.mdx:233 “- Invalid arguments”; 2025-11-25/changelog.mdx:28; 2025-11-25/server/tools.mdx:471; internal/mcpserver/server.go:666-674.

ERR-18 2026 schema still shows -32602 for bad tool arguments

  • What. 2026 prose makes input-validation failures (a date in the wrong format, a value out of range) isError results. The 2026 schema’s InvalidParamsError doc lists “Tools: Unknown tool name or invalid tool arguments” and ships an example invalid-tool-arguments.json: -32602 "Invalid arguments for tool calculate: Missing required property 'expression'".
  • Where. 2026-07-28, prose against schema.
  • mcpx @ 05c78b2. Follows the prose: tool failures are isError (internal/mcpserver/server.go:666-674).
  • Value to mcpx. + low: clients, mcpx included, must accept both forms from upstreams.
  • Effort. S.
  • Risk. Low.
  • Detail. The reading that reconciles them draws the line between failing the CallToolRequest schema (a malformed request, -32602) and failing the tool’s inputSchema (isError). The prose never states that line, and “missing required property” is an inputSchema failure, so the example contradicts the prose either way.
  • Sources. 2026-07-28/server/tools.mdx:762 “- Input validation errors (e.g., date in wrong format, value out of range)”; schema/2026-07-28/schema.ts:366 “- Tools: Unknown tool name or invalid tool arguments”; schema/2026-07-28/examples/InvalidParamsError/invalid-tool-arguments.json:3.

Lifecycle and versioning

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  How peers agree on a revision: the legacy initialize handshake, 2026 per-request versioning, server/discover, dual-era rules.

Revisions 2024-11-05 through 2025-11-25 open every connection with an initialize handshake that fixes the version and capabilities for the connection’s life; 2026-07-28 removes the handshake, puts version, capabilities and identity in every request’s _meta, and adds server/discover. mcpx has to speak both eras in both directions, and two things matter most: its modern path agrees with itself but not with the schema (protocolVersions where the field is supportedVersions), so it cannot complete discovery with a conformant 2026 peer in either direction; and it refuses legacy clients that the legacy rules say it must answer with a counter-offer.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
LV-01initialize / notifications/initialized handshake, removed in 20262024-11-05 has 2026-07-28 removes has better replacement opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · opencode v1 ✓ · opencode v2 ✓ (default)✓ — served and spoken; legacy tried first as a client+ highSlow
LV-02InitializeRequest.params: protocolVersion, capabilities, clientInfo2024-11-05 has 2025-11-25 has 2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ (+_meta) · 26-07 —✓ — all three sent; missing protocolVersion accepted+ lowSlow
LV-032024-11-05 is not served; the floor is 2025-03-26impl deferred 2024-11-05 has24-11 ✓ hosts exist✗ — deliberate floor at 2025-03-26+ medMmed
LV-04Server echoes a supported requested version2024-11-05 has 2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✓ — 2025-03-26 and 2025-06-18 agreed as asked+ highSlow
LV-05Counter-offer for an older legacy version (2024-11-05)mcpx missing 2024-11-05 has specs conflict24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✗ — refused with -32022 instead of a counter-offer (#202)+ highShigh
LV-06Counter-offer for an unknown newer versionmcpx missing 2024-11-05 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✗ — 9999-01-01 refused; future dates count as modern (#202)+ medSmed
LV-07initialize asking for 2026-07-282026-07-28 has mcpx missing26-07 ✓ (dual-era rule)✗ — refused -32022, legacy list only; rule says counter-offer (#202)+ lowSlow
LV-08Client checks the version the server answeredspecs conflict mcpx missing opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · opencode v1 ✓ · opencode v2 ✓✗ — stores any version the server returns (#203)+ medSmed
LV-09Version each client offers in initialize2025-11-25 has opencode v1 has opencode v2 has lootbox hasmcpx 2025-11-25 · opencode v1/v2 2025-11-25 · lootbox 2025-06-18partial — always 2025-11-25; documented override does not exist+ lowSlow
LV-10Pre-initialization restrictions (only ping, logging)2024-11-05 has 2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —acc. — any method served before initialize, as 2025-03-26+ lowSlow
LV-11“Respect negotiated version and capabilities”: SHOULD → MUST2025-06-18 has24-11 SHOULD · 25-03 SHOULD · 25-06 MUST · 25-11 MUST · 26-07 per requestpartial — results downgraded; some declarations leak+ highMmed
LV-12initialize MUST NOT be batched2025-03-26 has 2025-06-18 removes25-03 ✓ onlyn/a — mcpx accepts no batches at all+ lowSlow
LV-13notifications/initialized arriving at a 2026 server2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✓ — swallowed silently, bare initialized too+ lowSlow
LV-14instructions: InitializeResult → DiscoverResult2024-11-05 has 2026-07-28 has specs conflict opencode v1 has opencode v2 has24-11 ✓(schema) · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (discover) · opencode v1/v2 system prompt✓ — sent on both; client stores both+ highSlow
LV-15Implementation.title2025-06-18 has24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — not sent (optional)+ lowSlow
LV-16Implementation.description2025-11-25 has25-11 ✓ · 26-07 ✓✗ — not sent (optional)+ lowSlow
LV-17Implementation.icons2025-11-25 has25-11 ✓ · 26-07 ✓✗ — not sent, not read+ lowSlow
LV-18Implementation.websiteUrl2025-11-25 has25-11 ✓ · 26-07 ✓✗ — not sent (optional)+ lowSlow
LV-19Per-request _meta io.modelcontextprotocol/protocolVersion2026-07-28 has opencode v2 has26-07 ✓ · opencode v2 (modern era)✓ — read and checked per request; client sends it+ highSlow
LV-20Per-request _meta io.modelcontextprotocol/clientCapabilities2026-07-28 has opencode v2 has26-07 ✓ · opencode v2 (modern era)partial — read per request; a missing key means “none”+ highSmed
LV-21Per-request _meta io.modelcontextprotocol/clientInfo2026-07-28 has opencode v2 has26-07 ✓ (SHOULD) · opencode v2 (modern era)✓ — client sends it; server does not read it+ lowSlow
LV-22io.modelcontextprotocol/serverInfo in every result’s _meta2026-07-28 has mcpx missing26-07 ✓ (SHOULD)✗ — never emitted; client never reads it (#199)+ medSlow
LV-23DiscoverResult has no top-level serverInfo2026-07-28 has mcpx missing26-07 (field absent)✗ — sends undefined field; client reads it there (#199)+ lowSlow
LV-24A 2026 request missing required _meta is malformed2026-07-28 has mcpx missing26-07 ✓partial — no version ⇒ served as 2025-03-26; no caps ⇒ none (#199)+ medSmed
LV-25server/discover MUST be implemented2026-07-28 has opencode v2 has26-07 ✓ · opencode v2 (protocol auto/pinned)partial — answered in any era; field names wrong+ highShigh
LV-26DiscoverResult.supportedVersions2026-07-28 has mcpx missing26-07 ✓✗ — protocolVersions on both server and client (#199)+ highShigh
LV-27stdio probe order: server/discover before initialize2026-07-28 has impl deferred opencode v2 has26-07 SHOULD · opencode v2 autopartial — legacy first by default; modern first is opt-in+ medSmed
LV-28Which probe errors mean “modern server”2026-07-28 has mcpx missing26-07 ✓✗ — only a -32022 substring counts (#200)+ medSmed
LV-29Modern-only server refusing initialize; mcpx client aborts2026-07-28 has mcpx missing26-07 SHOULD name versions✗ — client aborts on -32022; never tries discover (#200)+ highShigh
LV-30HTTP era detection from the 400 body2026-07-28 has26-07 MAY/SHOULDpartial — status ignored; body matched by substring+ medSmed
LV-31Era is a property of the server; cache it2026-07-28 has26-07 SHOULD cache, MAY persistpartial — per connection; not persisted across restarts+ lowSlow
LV-32Per-server era settingopencode v2 hasmcpx four values · opencode v2 three valuespartial — typos silently fall back to legacy+ lowSlow
LV-33Dual-era server: era chosen per opening message2026-07-28 has26-07 MAY✓ — per-request _meta first, else connection handshake+ highSlow
LV-34ping received from a modern client2026-07-28 removes has better replacement24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —acc. — answered {resultType:"complete"}+ lowSlow
LV-35ping sent by a client2024-11-05 has 2026-07-28 removes lootbox has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · lootbox (fork)partial — Ping() era-blind but unused; answers server pings+ lowSlow
LV-36resultType required on every 2026 result2026-07-28 has26-07 ✓✓ — stamped for modern peers, stripped for legacy+ highSlow
LV-37Request-id uniqueness: per session → among in-flight2026-07-28 has24-11..25-11 session · 26-07 in flight✓ — server-initiated ids are negative+ lowSlow
LV-38stdio server exits promptly on stdin EOF2026-07-28 has26-07 SHOULD✓ — mcpx serve exits at EOF+ medSlow
LV-39Client stdio shutdown: close stdin, wait, SIGTERM, SIGKILL2024-11-05 has mcpx missing opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v2 ✓partial — SIGTERM immediately after closing stdin (#203)+ lowSlow
LV-40Restart a stdio server that exited unexpectedly2026-07-28 has lootbox has26-07 SHOULD · lootbox (fork) · opencode v1/v2 —✓ — lazy respawn on next call, backoff after failures+ medSlow

LV-01 initialize / notifications/initialized handshake, removed in 2026

  • What. In the four legacy revisions every connection opens with an initialize request, its InitializeResult, and a notifications/initialized notification, and that exchange MUST be the first interaction. 2026-07-28 deletes both message types; per-request _meta (LV-19..LV-21) and the optional server/discover (LV-25) replace them.
  • Where. 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25. Not in 2026-07-28: ClientRequest lists DiscoverRequest and no InitializeRequest, and ClientNotification is CancelledNotification alone. opencode v1 always initializes; opencode v2 does unless its per-server protocol option says otherwise (LV-32).
  • mcpx @ 05c78b2. The server answers initialize (internal/mcpserver/server.go:505-527) and swallows notifications/initialized (internal/mcpserver/server.go:540-541). The client initializes first by default (internal/mcpclient/client.go:231-232) and sends notifications/initialized after the result (internal/mcpclient/client.go:277).
  • Value to mcpx. + high: every published legacy client and server uses it, and 2026 explicitly allows a server to keep answering it (LV-33).
  • Effort. S — done in both directions.
  • Risk. Removing it would cut off the whole legacy ecosystem; keeping it costs nothing.
  • Detail. mcpx also accepts the non-standard bare method name initialized, an accept-liberally extra. What a modern-only server does with a stray notifications/initialized is LV-13.
  • Sources. 2024-11-05/basic/lifecycle.mdx:40 “The initialization phase MUST be the first interaction”; 2026-07-28/changelog.mdx:14 “remove the initialize/notifications/initialized handshake”; schema/2026-07-28/schema.ts:3153 “export type ClientRequest =”; schema/2026-07-28/schema.ts:3166 “export type ClientNotification = CancelledNotification;”; v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:36; v2:packages/core/src/mcp/client.ts:312; internal/mcpserver/server.go:540.

LV-02 InitializeRequest.params: protocolVersion, capabilities, clientInfo

  • What. protocolVersion: string, capabilities: ClientCapabilities and clientInfo: Implementation are required and unchanged in every legacy revision. 2025-11-25 moves them into InitializeRequestParams extends RequestParams, which formally allows _meta on initialize.
  • Where. 2024-11-05..2025-11-25. 2026-07-28 moves each into a per-request _meta key (LV-19, LV-20, LV-21).
  • mcpx @ 05c78b2. The client sends all three (internal/mcpclient/client.go:262-267). The server reads protocolVersion and capabilities (internal/mcpserver/server.go:506-517) and ignores clientInfo. An initialize with no protocolVersion, or with params that do not parse, is accepted and answered with 2025-11-25 (internal/mcpserver/server.go:848).
  • Value to mcpx. + low: nothing to add.
  • Effort. S — done.
  • Risk. None.
  • Detail. The schema describes protocolVersion as the latest version the client supports and adds “The client MAY decide to support older versions as well.” The 2026 mapping is protocolVersion → io.modelcontextprotocol/protocolVersion (required), capabilities → io.modelcontextprotocol/clientCapabilities (required), clientInfo → io.modelcontextprotocol/clientInfo (optional, SHOULD). Accepting a missing protocolVersion is accept-liberally: the field is required in every legacy schema.
  • Sources. schema/2024-11-05/schema.ts:151 “protocolVersion: string;”; schema/2025-11-25/schema.ts:255 “export interface InitializeRequestParams extends RequestParams {”; 2026-07-28/basic/index.mdx:375; internal/mcpserver/server.go:848.

LV-03 2024-11-05 is not served; the floor is 2025-03-26

  • What. mcpx serves 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28. A 2024-11-05 host cannot connect, and a request that declares nothing is assumed to be 2025-03-26.
  • Where. 2024-11-05 hosts exist (older desktop builds, early SDKs); the spec repo still carries its schema and its HTTP+SSE transport.
  • mcpx @ 05c78b2. Supported lists four revisions (internal/mcpserver/server.go:778) and Oldest is 2025-03-26 (internal/mcpserver/revisions.go:14). docs/protocol.md:51-59 records the floor as a deliberate choice. As a client mcpx still accepts a 2024-11-05 answer from an upstream (LV-08).
  • Value to mcpx. + med: reach to the oldest hosts, and “every revision” is the stated goal. − another downgrade() column to maintain.
  • Effort. M — the version, a downgrade column (no audio, no tool annotations, no completions capability), and the HTTP+SSE transport if those hosts are to be reached remotely (transports.md TR-10).
  • Risk. Not done: the “speak every revision” goal stays unmet for 2024-11-05.
  • Detail. docs/protocol.md:51-53 says the 2025-03-26 schema “was not among” those read; it is available now and confirms the doc’s assumptions (audio, annotations and completions present; no structuredContent, resource_link, elicitation or tasks). Serving 2024-11-05 is a separate decision from counter-offering to a 2024-11-05 client (LV-05), which needs no new column.
  • Sources. internal/mcpserver/revisions.go:14 “const Oldest = "2025-03-26"”; docs/protocol.md:56 “as the floor”; internal/mcpserver/server.go:778.

LV-04 Server echoes a supported requested version

  • What. In every legacy revision, if the server supports the version the client asked for, it MUST answer with that same version.
  • Where. 2024-11-05..2025-11-25; no negotiation exists in 2026-07-28 (LV-19).
  • mcpx @ 05c78b2. negotiate returns the requested version when it is a supported legacy one (internal/mcpserver/server.go:855) and the reply echoes it (internal/mcpserver/server.go:523), with capabilities shaped for that revision. Wire W2 agreed 2025-03-26; the stdio run S1 agreed 2025-06-18.
  • Value to mcpx. + high: this is the common path for every legacy host.
  • Effort. S — done.
  • Risk. None.
  • Detail. The agreed version is stored on the connection and governs everything sent to that peer afterwards (internal/mcpserver/conn.go:101-105).
  • Sources. 2024-11-05/basic/lifecycle.mdx:126 “If the server supports the requested protocol version, it MUST respond with the same”; internal/mcpserver/server.go:855; wire W2.

LV-05 Counter-offer for an older legacy version (2024-11-05)

  • What. Every legacy revision says that when the server does not support the requested version it MUST still answer initialize successfully, with another version it supports (SHOULD be its latest); the client then decides whether to disconnect. Negotiation is a counter-offer, not an error.
  • Where. 2024-11-05..2025-11-25, identical text.
  • mcpx @ 05c78b2. negotiate() returns "" for any version not in Supported (internal/mcpserver/server.go:844-859) and the initialize branch answers unsupportedVersion(..., legacyOnly=true) (internal/mcpserver/server.go:506-509). Wire W1 (HTTP) and S1 (stdio): -32022 with data.requested: "2024-11-05" and supported = the three legacy versions. The same failed handshake also mints an Mcp-Session-Id (transports.md TR-27). docs/protocol.md:76 claims initialize accepts “any legacy version”.
  • Value to mcpx. + high: a 2024-11-05 host would be offered 2025-11-25 and could proceed if it tolerates it; today it gets a code it has never heard of.
  • Effort. S — return Latest instead of "" for a well-formed version mcpx does not implement.
  • Risk. Not done: every 2024-11-05-only host fails the handshake. Done: such a host may disconnect (spec-intended), or proceed and meet 2025-era shapes it did not ask for, which is its decision to make.
  • Detail. The same lifecycle pages show an “initialization error” example with -32602 and data: {supported, requested} (errors.md ERR-10), which is why the texts conflict. The example’s requested value is "1.0.0", not a date, which is one way to reconcile them: counter-offer for a well-formed version, error for garbage.
  • Sources. 2024-11-05/basic/lifecycle.mdx:126; 2025-03-26/basic/lifecycle.mdx:132 “Otherwise, the server MUST respond with another protocol version it”; 2025-11-25/basic/lifecycle.mdx:170; internal/mcpserver/server.go:858 return ""; docs/protocol.md:76; wire W1, S1.

LV-06 Counter-offer for an unknown newer version

  • What. A legacy handshake from a client newer than mcpx (for example 9999-01-01) gets the same counter-offer rule as LV-05: answer with mcpx’s own latest legacy version.
  • Where. Required by every legacy revision.
  • mcpx @ 05c78b2. Same refusal path as LV-05. Wire W4: HTTP 200, -32022, supported = the legacy list, and a new Mcp-Session-Id header.
  • Value to mcpx. + med: forward compatibility with the next client that still starts with initialize.
  • Effort. S — the same change as LV-05, plus the Modern() check below.
  • Risk. Not done: a future dual-era client that probes with initialize first falls over at the handshake.
  • Detail. Modern() is a string comparison, version >= "2026-07-28" (internal/mcpserver/server.go:793), so every future date counts as “modern” and would still be refused over initialize after LV-05 is fixed, unless that check changes too.
  • Sources. 2025-11-25/basic/lifecycle.mdx:170; internal/mcpserver/server.go:793 “func Modern(version string) bool { return version >= "2026-07-28" }”; wire W4.

LV-07 initialize asking for 2026-07-28

  • What. 2026 has no initialize. A dual-era server treats an initialize request as selecting legacy semantics, so an initialize carrying protocolVersion: "2026-07-28" falls under the legacy counter-offer rule and should be answered with 2025-11-25.
  • Where. 2026-07-28 versioning page (dual-era rules).
  • mcpx @ 05c78b2. The initialize branch passes legacyOnly=true (internal/mcpserver/server.go:237-242, :508). Wire W3: -32022, supported: ["2025-11-25","2025-06-18","2025-03-26"]. docs/protocol.md:76 records the refusal as intended.
  • Value to mcpx. + low: either answer is consistent with “initialize means legacy”. A counter-offer lets a confused client proceed; the refusal’s data omits 2026-07-28, so it gives no hint to try server/discover.
  • Effort. S — the same change as LV-05.
  • Risk. Low.
  • Detail. The refusal code is itself 2026-only (errors.md ERR-10). A dual-era client that receives it concludes “modern server” and retries with server/discover, which mcpx answers, so the exchange still ends in the modern era after one wasted round trip.
  • Sources. 2026-07-28/basic/versioning.mdx:178 “- An initialize request selects legacy semantics, scoped to the stdio”; internal/mcpserver/server.go:508; docs/protocol.md:76; wire W3.

LV-08 Client checks the version the server answered

  • What. Legacy prose says the client SHOULD disconnect if it does not support the version in the server’s InitializeResult; the schema doc comment on InitializeResult.protocolVersion says MUST.
  • Where. Both texts in all four legacy revisions. opencode v1 and v2 accept only a known list (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07) and fail the connection on anything else.
  • mcpx @ 05c78b2. The client stores ir.ProtocolVersion as Negotiated with no check (internal/mcpclient/client.go:275). /v1/protocol showed the test upstream at negotiated: 2025-06-18 (wire, “mcpx as client”). A 2024-11-05 answer would be accepted although mcpx implements neither that revision’s shapes nor its HTTP+SSE transport.
  • Value to mcpx. + med: prevents silently misparsing a server that picked a revision mcpx cannot speak, while accepting known lower versions stays accept-liberally.
  • Effort. S — check against Supported and disconnect otherwise.
  • Risk. Not done: silent misparse against an old or unknown revision.
  • Detail. The schema is the source of truth for a revision, so treat it as MUST. Nothing below the handshake adapts to the accepted version either: the HTTP version header stays 2025-11-25 (transports.md TR-34).
  • Sources. 2024-11-05/basic/lifecycle.mdx:130 “If the client does not support the version in the server’s response, it SHOULD”; schema/2024-11-05/schema.ts:162 “If the client cannot support this version, it MUST disconnect.”; schema/2025-11-25/schema.ts:281; internal/mcpclient/client.go:275; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/types.js L4; https://unpkg.com/@modelcontextprotocol/core@2.0.0/dist/auth-CUe6YdwF.mjs L4–L12.

LV-09 Version each client offers in initialize

  • What. Which revision a client proposes when it opens a legacy connection.
  • Where. mcpx: 2025-11-25. opencode v1 (@modelcontextprotocol/sdk 1.29.0 plus a 647-line patch) and opencode v2 (@modelcontextprotocol/client 2.0.0, unless protocol is set): 2025-11-25. lootbox (@modelcontextprotocol/sdk 1.22.0): 2025-06-18 as its latest, with client capabilities {}.
  • mcpx @ 05c78b2. ProtocolVersion = "2025-11-25" (internal/mcpclient/client.go:22), sent at internal/mcpclient/client.go:263; the modern list is ["2026-07-28"] (internal/mcpclient/client.go:26). docs/protocol.md:39-40 says 2025-03-26 and 2025-06-18 are spoken upstream “on request (protocol: force-legacy)”, but force-legacy only removes the fallback; it never chooses a version.
  • Value to mcpx. + low: offering the newest legacy revision is right. A per-server pin would matter only for servers that mishandle counter-offers.
  • Effort. S — correct the doc, or add the pin it describes.
  • Risk. A documented knob that does not exist is a declared-but-not-delivered bug.
  • Detail. lootbox’s capabilities: {} means an upstream elicitation/create or roots/list gets the SDK’s default method-not-found. opencode v1’s patch moves the handshake into _initialize so it can be re-run on session expiry (transports.md TR-29).
  • Sources. internal/mcpclient/client.go:22 “const ProtocolVersion = "2025-11-25"”; docs/protocol.md:39 “on request (protocol: force-legacy)”; v1:packages/opencode/package.json:83; v2:packages/core/package.json:113; v2:packages/core/src/mcp/client.ts:312; .lootbox/deno.json:44; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153; @modelcontextprotocol/sdk@1.22.0/dist/esm/types.js:2 “export const LATEST_PROTOCOL_VERSION = ‘2025-06-18’;” (the copy lootbox resolves from the local Deno npm cache).

LV-10 Pre-initialization restrictions (only ping, logging)

  • What. Legacy: the client SHOULD NOT send requests other than ping before the initialize response, and the server SHOULD NOT send requests other than ping and logging before notifications/initialized.
  • Where. 2024-11-05..2025-11-25; meaningless in 2026-07-28.
  • mcpx @ 05c78b2. The server processes any method on a connection that never initialized and treats it as Oldest, 2025-03-26 (internal/mcpserver/conn.go:140-146).
  • Value to mcpx. + low: SHOULD NOT, so serving is permitted.
  • Effort. S — nothing to do.
  • Risk. A modern client that forgot _meta is silently served legacy shapes (LV-24).
  • Detail. 2026’s stdio page notes that legacy servers which do not validate ordering answer an era-ambiguous method under legacy semantics, which is why the modern probe treats any non-modern answer as “legacy” (LV-27, LV-28). The dual-era case of a request with neither _meta nor a prior handshake is covered in the process-model register.
  • Sources. 2024-11-05/basic/lifecycle.mdx:113 “The client SHOULD NOT send requests other than”; 2024-11-05/basic/lifecycle.mdx:116; 2026-07-28/basic/transports/stdio.mdx:144 “some legacy servers do not validate that a”; internal/mcpserver/conn.go:144 “v = Oldest”.

LV-11 “Respect negotiated version and capabilities”: SHOULD → MUST

  • What. 2024-11-05 and 2025-03-26 say both parties SHOULD respect the negotiated version and use only negotiated capabilities; 2025-06-18 makes it MUST. 2026-07-28 restates it per request: a server MUST NOT rely on capabilities the client has not declared on that request.
  • Where. MUST from 2025-06-18.
  • mcpx @ 05c78b2. Defines() and downgrade() gate result shapes per revision (internal/mcpserver/revisions.go:115-123, :132-170). Declarations are not covered by that machinery; the capability leaks to 2025-11-25 and 2026 peers are in the capabilities register.
  • Value to mcpx. + high: this is mcpx’s “send conservatively” rule.
  • Effort. M — the remaining leaks are spread across capabilities, lists and requests.
  • Risk. Strict peers reject or misread fields they never negotiated.
  • Detail. The 2025-06-18 changelog records the change as item 9.
  • Sources. 2024-11-05/basic/lifecycle.mdx:162 “Both parties SHOULD:”; 2025-06-18/basic/lifecycle.mdx:175 “Both parties MUST:”; 2025-06-18/changelog.mdx:31; internal/mcpserver/revisions.go:132-170.

LV-12 initialize MUST NOT be batched

  • What. With JSON-RPC batching introduced, 2025-03-26 forbids putting initialize in a batch; the sentence goes away with batching in 2025-06-18.
  • Where. 2025-03-26 only.
  • mcpx @ 05c78b2. mcpx does not accept batches at all (transports.md TR-09).
  • Value to mcpx. + low: only relevant if 2025-03-26 batching is implemented.
  • Effort. S.
  • Risk. None.
  • Detail. The stated reason was compatibility with 2024-11-05 peers that cannot read batches.
  • Sources. 2025-03-26/basic/lifecycle.mdx:72 “The initialize request MUST NOT be part of a JSON-RPC”.

LV-13 notifications/initialized arriving at a 2026 server

  • What. 2026 defines no such notification, so a modern-only receiver sees an unknown notification. Notifications never get a response; on HTTP the server returns 202 if it accepts the POST or a 4xx status.
  • Where. Required after initialize in 2024-11-05..2025-11-25; absent in 2026-07-28.
  • mcpx @ 05c78b2. Accepted silently, as are bare initialized notifications (internal/mcpserver/server.go:540-541). The mcpx client sends it only after a legacy initialize succeeded, so it never sends it to a modern-only server.
  • Value to mcpx. + low: correct for a dual-era server.
  • Effort. S — done.
  • Risk. None.
  • Detail. 2026 HTTP says header requirements for notification POSTs are “not defined by this revision”, so a modern-only HTTP server might 400 it for a missing Mcp-Method; a legacy client would already have failed at initialize.
  • Sources. schema/2026-07-28/schema.ts:3166; 2026-07-28/basic/index.mdx:160 “The receiver MUST NOT send a response.”; 2026-07-28/basic/transports/streamable-http.mdx:102; internal/mcpserver/server.go:540.

LV-14 instructions: InitializeResult → DiscoverResult

  • What. Optional natural-language guidance from the server about how to use it. Legacy servers put it in InitializeResult.instructions; 2026 servers in DiscoverResult.instructions.
  • Where. Present in every legacy schema including 2024-11-05, although the lifecycle prose first shows it in 2025-03-26. 2026-07-28 moves it to DiscoverResult. opencode v1 and v2 render server instructions inside <mcp_instructions> in the system prompt; v2 also reads them from server/discover and adds a code-mode hint.
  • mcpx @ 05c78b2. Sent in both handshakes (internal/mcpserver/server.go:526, :537); the client stores either (internal/mcpclient/client.go:274, :322).
  • Value to mcpx. + high: mcpx’s whole pitch (use mcpx_exec, not a hundred tools) reaches the model this way, and opencode puts it in front of the model on every turn.
  • Effort. S — done.
  • Risk. Long instructions cost tokens on every opencode turn. A 2026 client that never calls server/discover (it is optional for clients) never sees them.
  • Detail. 2026 tightens the wording: instructions “should not duplicate information already in tool descriptions”. opencode v1 drops servers whose tools are all permission-disabled; v2 delivers instruction changes as diffs. The schema-vs-prose gap for 2024-11-05 matters only to someone implementing from prose.
  • Sources. schema/2024-11-05/schema.ts:172 “instructions?: string;”; 2025-03-26/basic/lifecycle.mdx:103; schema/2026-07-28/schema.ts:696; internal/mcpserver/server.go:526; internal/mcpclient/client.go:322; v1:packages/opencode/src/session/system.ts:129; v2:packages/core/src/mcp/instructions.ts:23; v2:packages/core/src/mcp/client.ts:249.

LV-15 Implementation.title

  • What. Implementation (the type of clientInfo and serverInfo) gains an optional display title beside the programmatic name, through BaseMetadata.
  • Where. {name, version} only in 2024-11-05 and 2025-03-26; {name, title?, version} from 2025-06-18.
  • mcpx @ 05c78b2. Sends {name, version} as server and client (internal/mcpserver/server.go:525, internal/mcpclient/modern.go:84).
  • Value to mcpx. + low: a UI nicety.
  • Effort. S.
  • Risk. None; optional.
  • Detail. Send conservatively: no title to a 2024-11-05 or 2025-03-26 peer (harmless in practice, since the object is open).
  • Sources. schema/2025-06-18/schema.ts:331 “export interface Implementation extends BaseMetadata {”; schema/2025-03-26/schema.ts:277 “export interface Implementation {”; 2025-06-18/basic/lifecycle.mdx:69.

LV-16 Implementation.description

  • What. Optional free-text description on Implementation.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Not sent (internal/mcpserver/server.go:525); the client does not surface an upstream’s.
  • Value to mcpx. + low: an upstream’s description would improve mcpx’s catalog output.
  • Effort. S.
  • Risk. None.
  • Detail. Of the three Implementation fields added in 2025-11-25, this is the only one the changelog names.
  • Sources. 2025-11-25/changelog.mdx:25 “Add optional description field to Implementation”; schema/2025-11-25/schema.ts:550.

LV-17 Implementation.icons

  • What. Implementation extends the Icons mixin, so a client or server can publish icons for itself.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Not sent; not read from upstreams.
  • Value to mcpx. + low.
  • Effort. S to send; more to render safely.
  • Risk. Rendering upstream icons without the icon rules below.
  • Detail. Not named in the changelog; it arrives through the mixin. Consumers MUST reject icon URIs with unsafe schemes (javascript:, file:, ftp:, ws:, local app schemes), should fetch without credentials, and should check that icons come from the server’s origin.
  • Sources. schema/2025-11-25/schema.ts:550 “export interface Implementation extends BaseMetadata, Icons {”; 2025-11-25/basic/index.mdx:247; 2025-11-25/basic/index.mdx:251; 2025-11-25/basic/index.mdx:252.

LV-18 Implementation.websiteUrl

  • What. Optional websiteUrl (@format uri) on Implementation.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Not sent (internal/mcpserver/server.go:525).
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. None.
  • Detail. The 2025-11-25 changelog never mentions it.
  • Sources. schema/2025-11-25/schema.ts:567 “websiteUrl?: string;”.

LV-19 Per-request _meta io.modelcontextprotocol/protocolVersion

  • What. Every 2026 request MUST carry its protocol version in params._meta["io.modelcontextprotocol/protocolVersion"]. There is no negotiation step: the server accepts or rejects each request on its own, and the version of the request is the version of the response.
  • Where. 2026-07-28. opencode v2’s SDK stamps it on every request of a modern connection.
  • mcpx @ 05c78b2. The server reads it (requestVersion, internal/mcpserver/server.go:813-822) and refuses an unsupported one before dispatch (internal/mcpserver/server.go:469-471; the code is errors.md ERR-09). The client adds it to every modern request (internal/mcpclient/modern.go:82).
  • Value to mcpx. + high: the core of the modern era.
  • Effort. S — done.
  • Risk. None.
  • Detail. The key is non-optional in the schema. On HTTP it MUST equal the MCP-Protocol-Version header: the schema doc says “400 Bad Request”, the transport page says 400 with -32020 (errors.md ERR-07). Absent means malformed (LV-24).
  • Sources. 2026-07-28/basic/versioning.mdx:12 “There is no negotiation handshake. Every request carries its protocol”; schema/2026-07-28/schema.ts:76; schema/2026-07-28/schema.ts:71-72; internal/mcpserver/server.go:469; internal/mcpclient/modern.go:82; https://unpkg.com/@modelcontextprotocol/core@2.0.0/dist/auth-CUe6YdwF.mjs L24; v2:packages/core/src/mcp/client.ts:248.

LV-20 Per-request _meta io.modelcontextprotocol/clientCapabilities

  • What. Client capabilities are declared on every 2026 request; {} means “no optional capabilities”, and the server MUST NOT carry them over from earlier requests.
  • Where. 2026-07-28 (legacy declares once, in initialize). opencode v2’s SDK sends it in the modern era.
  • mcpx @ 05c78b2. The server builds a per-request Peer from _meta (internal/mcpserver/conn.go:133-136, :150-166); if the key is missing the capabilities are nil and the request is served rather than rejected (LV-24). The client always sends its own (internal/mcpclient/modern.go:83).
  • Value to mcpx. + high: one connection can carry many agents with different capabilities.
  • Effort. S.
  • Risk. A proxy that caches capabilities per connection breaks the MUST NOT.
  • Detail. The mcpx client declares roots: {listChanged: false} in the modern era (internal/mcpclient/modern.go:52) although 2026 roots is {} and deprecated; the shape of what mcpx declares is in the capabilities register. Because capabilities are per request, mcpx could declare upstream only what the originating host declared (for example url-mode elicitation); today its declaration is fixed per client.
  • Sources. schema/2026-07-28/schema.ts:96 “Servers MUST NOT infer capabilities from prior requests.”; 2026-07-28/basic/index.mdx:377; internal/mcpserver/conn.go:133; internal/mcpclient/modern.go:83.

LV-21 Per-request _meta io.modelcontextprotocol/clientInfo

  • What. Client identity moves to optional per-request _meta; clients SHOULD send it on every request unless configured not to. It is self-reported and display-only (meta.md META-08).
  • Where. 2026-07-28. opencode v2’s SDK sends it in the modern era.
  • mcpx @ 05c78b2. The client sends it on every modern request (internal/mcpclient/modern.go:84). The server defines the key (internal/mcpserver/conn.go:174) but never reads it.
  • Value to mcpx. + low: attributing upstream calls per agent in logs is allowed; changing behaviour on it is not.
  • Effort. S.
  • Risk. Using it for authorization or isolation would break the SHOULD NOT.
  • Detail. SEP-2575 had it required; PR #3002 made it optional after the SEP reached Final.
  • Sources. 2026-07-28/basic/index.mdx:384 “Clients SHOULD include io.modelcontextprotocol/clientInfo on every request”; seps/2575-stateless-mcp.md:791; internal/mcpclient/modern.go:84; internal/mcpserver/conn.go:174.

LV-22 io.modelcontextprotocol/serverInfo in every result’s _meta

  • What. A 2026 server SHOULD identify itself on every result under result._meta["io.modelcontextprotocol/serverInfo"], DiscoverResult included.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Never emitted: HandleOn adds only resultType (internal/mcpserver/server.go:432-436; wire W6, W7, W8). The client reads identity only from a top-level field (LV-23).
  • Value to mcpx. + med: a modern client that wants to show which server answered finds nothing, and with LV-23 it has no identity from mcpx at all.
  • Effort. S — stamp it where resultType is stamped, for modern peers only.
  • Risk. Low (SHOULD).
  • Detail. Legacy results must not get it. PR #3002 removed DiscoverResult.serverInfo “to avoid duplicate representations”. The 2026 reserved-key table omits this key although the schema defines it (meta.md META-06).
  • Sources. 2026-07-28/basic/index.mdx:396 “Servers SHOULD include the following io.modelcontextprotocol/* field in”; schema/2026-07-28/schema.ts:157; seps/2575-stateless-mcp.md:795; 2026-07-28/changelog.mdx:14; internal/mcpserver/server.go:432-436; wire W6.

LV-23 DiscoverResult has no top-level serverInfo

  • What. 2026’s DiscoverResult defines supportedVersions, capabilities, instructions and the cache fields. Server identity belongs in result _meta (LV-22), not in a serverInfo field.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The server sends a top-level serverInfo (internal/mcpserver/server.go:535; wire W5, W9) and the client reads it from there (internal/mcpclient/client.go:285, :320).
  • Value to mcpx. + low on its own; it is the other half of LV-22.
  • Effort. S.
  • Risk. A validator that treats the result as closed rejects it; an open one ignores it and finds no identity.
  • Detail. Sending a field the peer’s revision does not define is a send-conservatively conflict.
  • Sources. schema/2026-07-28/schema.ts:678-697; internal/mcpserver/server.go:535; internal/mcpclient/client.go:285; wire W5.

LV-24 A 2026 request missing required _meta is malformed

  • What. A 2026 request without protocolVersion or clientCapabilities in _meta is malformed; the server MUST reject it with -32602, and on HTTP with status 400.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Not rejected. No version: treated as legacy at 2025-03-26 (internal/mcpserver/conn.go:134-146). Version present but no capabilities: capabilities nil, request served (internal/mcpserver/conn.go:150-166). Wire W9: a bare server/discover with no _meta and no headers was answered like W5 and issued a session.
  • Value to mcpx. + med: conformance for modern peers. − rejecting every request without _meta would break lenient legacy clients that skip initialize, so reject only when protocolVersion is present and clientCapabilities is not.
  • Effort. S.
  • Risk. As is, a modern client with a bug silently gets legacy-shaped results.
  • Detail. 2026 defines behaviour for “a request carrying modern _meta” and for “an initialize request”; a dual-era server receiving neither is not covered (see the process-model register). On HTTP, a server supporting pre-2025-06-18 clients MAY treat a missing version header as 2025-03-26, which matches mcpx’s default. The same malformed request can also draw -32020 for the missing header; the order is unspecified (errors.md ERR-14).
  • Sources. 2026-07-28/basic/index.mdx:380 “A request missing any required field is malformed; the server MUST reject it with”; 2026-07-28/basic/index.mdx:381; internal/mcpserver/conn.go:144; wire W9.

LV-25 server/discover MUST be implemented

  • What. Every 2026 server MUST answer server/discover, which returns supported versions, capabilities, optional instructions and identity. Clients MAY call it; it is the recommended stdio probe (LV-27). The request carries nothing but _meta, yet params itself is required.
  • Where. 2026-07-28. opencode v2 calls it when a server’s protocol is auto or pinned to 2026-07-28.
  • mcpx @ 05c78b2. Answered in any era (internal/mcpserver/server.go:529-538; wire W5, W9), with capabilities in the 2026 shape. The client probes it (internal/mcpclient/client.go:296).
  • Value to mcpx. + high: without a conformant answer no modern client can learn what mcpx speaks.
  • Effort. S — the field fixes are LV-23 and LV-26.
  • Risk. High while the field names are wrong: modern interop is broken in both directions.
  • Detail. DiscoverResult extends CacheableResult, so ttlMs and cacheScope are required; mcpx sends neither, and the 2026 changelog’s cacheable-result item forgets to list server/discover (both covered in the caching register). The capability object mcpx puts in the answer, including a core tasks object 2026 does not define, is covered in the capabilities register. Answering it on a legacy connection is accept-liberally.
  • Sources. 2026-07-28/server/discover.mdx:8 “capabilities, and identity before sending any other requests. Servers MUST”; schema/2026-07-28/schema.ts:665-668; schema/2026-07-28/schema.ts:678 “export interface DiscoverResult extends CacheableResult {”; 2026-07-28/changelog.mdx:36; internal/mcpserver/server.go:529-538; v2:packages/core/src/mcp/client.ts:313.

LV-26 DiscoverResult.supportedVersions

  • What. The version list in DiscoverResult is supportedVersions: string[]; the client picks one for its subsequent requests.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Both halves use the wrong name. The server answers "protocolVersions": Supported (internal/mcpserver/server.go:534); the client parses protocolVersions (internal/mcpclient/client.go:284), so against a conformant server its list is empty and it fails with “no shared protocol version; the server offers []” (internal/mcpclient/client.go:314-318). Tests pin the wrong name on both sides (internal/mcpserver/server_test.go:393, internal/mcpclient/era_test.go:87, internal/mcpclient/modern_test.go:88). Wire W5, W9.
  • Value to mcpx. + high: this is the whole “mcpx as a modern server” and “mcpx as a modern client” path.
  • Effort. S, plus the three tests.
  • Risk. High: mcpx↔mcpx works only because both halves are wrong the same way; every conformant modern peer fails.
  • Detail. The client’s selection logic is right (newest mutually supported version, not the first listed); only the key is wrong. The stdio probe text says to “Select a mutually supported version from supportedVersions”. The name protocolVersions may come from an early SEP draft. Over HTTP a modern upstream fails even earlier, on the missing Mcp-Method header (transports.md TR-37).
  • Sources. schema/2026-07-28/schema.ts:683 “supportedVersions: string[];”; internal/mcpserver/server.go:534 “"protocolVersions": Supported,”; internal/mcpclient/client.go:284; internal/mcpclient/client.go:314-318; wire W5.

LV-27 stdio probe order: server/discover before initialize

  • What. A dual-era stdio client SHOULD send server/discover, with its preferred modern version, before anything else: a DiscoverResult means modern; a recognised modern error means modern (retry with a version from supported, do not fall back); any other error or a timeout means legacy (fall back to initialize). Modern-only clients are also recommended to probe.
  • Where. 2026-07-28. opencode v2 with protocol: "auto" probes server/discover and falls back to legacy; its default is plain legacy.
  • mcpx @ 05c78b2. The default is the reverse, initialize first (internal/mcpclient/client.go:184, :231-232); modern-first is opt-in per server (LV-32). The comment at internal/mcpclient/client.go:193-197 defers the flip: “Legacy first is correct today and will stop being correct, which is why it is configurable”. docs/protocol.md:267 gives the same reason.
  • Value to mcpx. + med: the spec’s SHOULD. − legacy first saves a round trip on today’s mostly legacy servers.
  • Effort. S to flip the default, once LV-26 and LV-29 are fixed.
  • Risk. A modern-only stdio server that treats a bad initialize as fatal and exits makes legacy-first fail where probe-first works; with LV-29 unfixed, legacy-first is exactly what makes spec-following modern servers unreachable.
  • Detail. Legacy servers answer an unknown pre-initialize request with -32601, -32602 or nothing, hence “or times out”. The HTTP equivalent is LV-30. Cache the result (LV-31).
  • Sources. 2026-07-28/basic/transports/stdio.mdx:124 “legacy version that requires an initialize handshake SHOULD probe with”; internal/mcpclient/client.go:193-197; internal/mcpclient/client.go:232; docs/protocol.md:267; v2:packages/schema/src/mcp.ts:20; v2:packages/core/src/mcp/client.ts:313.

LV-28 Which probe errors mean “modern server”

  • What. A recognised modern error (for example -32022) identifies a modern server; everything else is treated as legacy, and the legacy fallback MUST NOT be keyed to one specific error code.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. isVersionError is strings.Contains(err.Error(), "-32022") (internal/mcpclient/client.go:327-331). A modern server’s -32020 or -32021 therefore triggers a legacy fallback, which a modern-only server also rejects. When server/discover itself draws -32022, the client aborts rather than retrying with a version from data.supported; that is moot today because mcpx knows one modern version (internal/mcpclient/client.go:26).
  • Value to mcpx. + med: correct era decisions against modern servers that reject for reasons other than the version.
  • Effort. S — parse the error code and treat -32020..-32022 as modern.
  • Risk. Wrong-era fallback and a misleading reported error.
  • Detail. A substring test also matches -32022 quoted anywhere in an error message. Pre-release SDKs that still send -32004 are not recognised, which is the reading the 2026 allocation policy requires anyway (errors.md ERR-11).
  • Sources. 2026-07-28/basic/transports/stdio.mdx:139 “The fallback MUST NOT be keyed to one specific error code: legacy servers”; internal/mcpclient/client.go:330 “strings.Contains(err.Error(), "-32022")”; internal/mcpclient/client.go:26.

LV-29 Modern-only server refusing initialize; mcpx client aborts

  • What. A modern-only server rejects initialize with an implementation-defined JSON-RPC error and SHOULD name the versions it supports, since a legacy client has no way to fall forward. A dual-era client that tried legacy first should fall forward to server/discover.
  • Where. 2026-07-28 versioning page and its compatibility matrix.
  • mcpx @ 05c78b2. newClient checks isVersionError after the first attempt regardless of which era it was (internal/mcpclient/client.go:244-249); the comment there assumes the first attempt was modern. So a modern server that answers initialize with -32022, as mcpx’s own server does, makes the client give up. The era tests use a fake that answers initialize with -32601, so the case is untested (internal/mcpclient/era_test.go:66-69).
  • Value to mcpx. + high: reachability of every modern-only server that follows the SHOULD.
  • Effort. S — short-circuit only when the first attempt was modern.
  • Risk. Not done: mcpx cannot connect to spec-following modern-only servers with its default settings.
  • Detail. Over HTTP the mcpx client’s initialize carries MCP-Protocol-Version but no Mcp-Method, so a modern-only HTTP server rejects it with 400 before method dispatch. If its header check runs first the body is -32020, which does not contain -32022, so the client does fall forward, and then fails again on the missing Mcp-Method (transports.md TR-37); if its version check runs first the body is -32022 and the client aborts as above. The spec does not order the two checks (errors.md ERR-14).
  • Sources. 2026-07-28/basic/versioning.mdx:154 “A server that supports only modern versions SHOULD name”; 2026-07-28/basic/versioning.mdx:170; internal/mcpclient/client.go:246 “if isVersionError(err) {”; internal/mcpclient/era_test.go:66-69.

LV-30 HTTP era detection from the 400 body

  • What. On Streamable HTTP a dual-era client MAY send a modern request first. On a 400 it SHOULD inspect the body: a recognised modern JSON-RPC error means modern (fix and retry); an empty or unrecognised body means fall back to initialize, and possibly further to HTTP+SSE (transports.md TR-11).
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The client uses the stdio logic (legacy first) and never consults the status for era (internal/mcpclient/client.go:235-258). A non-2xx answer becomes an error carrying the status and up to 4 KiB of body (internal/mcpclient/http.go:136-140), which isVersionError then greps.
  • Value to mcpx. + med: as a server, mcpx must return 400 with a JSON-RPC body for modern errors for this to work in its favour; it returns 200 (errors.md ERR-13) and a plain JSON body for header mismatches (errors.md ERR-07).
  • Effort. S.
  • Risk. Dual-era clients misclassify mcpx, and mcpx misclassifies servers that reject for header reasons.
  • Detail. Modern servers use 400 for -32602 (missing _meta), -32020, -32021 and -32022, and 404 with -32601 for unknown methods.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:654 “era the server implements by attempting a modern request first. On”; 2026-07-28/basic/transports/streamable-http.mdx:660; internal/mcpclient/http.go:136-140.

LV-31 Era is a property of the server; cache it

  • What. Era belongs to the server, not the request. Clients SHOULD cache it for the life of a stdio process or per HTTP origin, MAY persist it across restarts, and re-probe if the assumption fails.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Client.Era is set once per connection (internal/mcpclient/client.go:237, :257); each new client, one per pool instance, probes again, and nothing persists across daemon restarts. /v1/protocol reports each server’s era, negotiated version and preference (internal/daemon/routes_proto.go:515).
  • Value to mcpx. + low: mcpx spawns many instances of one server, and each repeats the probe.
  • Effort. S.
  • Risk. None.
  • Detail. For HTTP the key is the origin, not the URL path.
  • Sources. 2026-07-28/basic/versioning.mdx:148 “The era determination is a property of the server, not of an individual”; internal/mcpclient/client.go:237; internal/daemon/routes_proto.go:515.

LV-32 Per-server era setting

  • What. A per-upstream setting that chooses or forces the era.
  • Where. mcpx: protocol: legacy | modern | force-legacy | force-modern. opencode v2: protocol: "legacy" | "auto" | "2026-07-28", default legacy; a pinned version the server does not support fails with a message naming the versions it does, and a pin must be a modern revision. opencode v1 has no such option.
  • mcpx @ 05c78b2. Parsed at internal/config/config.go:142-146 and mapped at internal/pool/pool.go:636-646, whose default: branch turns any unknown value, typos included, into legacy; config loading validates only command and URL (internal/config/config.go:701-708).
  • Value to mcpx. + low: lets a user pin an era; typos should fail loudly.
  • Effort. S — validate at load.
  • Risk. Low: a misspelt protocol silently probes legacy first.
  • Detail. docs/opencode-plugin.md:187 says opencode v2 “negotiating 2026-07-28 per server” will exercise mcpx’s modern path; that happens only when the user sets protocol for mcpx, because v2 defaults to legacy.
  • Sources. internal/pool/pool.go:645 “return mcpclient.PreferLegacy”; internal/config/config.go:142-146; v2:packages/schema/src/mcp.ts:20 “Schema.Literals(["legacy", "auto", "2026-07-28"])”; v2:packages/schema/src/mcp.ts:23; docs/opencode-plugin.md:187.

LV-33 Dual-era server: era chosen per opening message

  • What. A request carrying modern _meta is served statelessly under 2026; an initialize selects legacy semantics scoped to that stdio process or HTTP session; a dual-era server MAY serve both eras concurrently on one endpoint or process.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The peer is resolved per request, from _meta first and the connection’s handshake otherwise (internal/mcpserver/conn.go:133-147); legacy capabilities are stored per connection (internal/mcpserver/conn.go:101-105).
  • Value to mcpx. + high: this is the “speak every revision” goal.
  • Effort. S — done.
  • Risk. None.
  • Detail. The rule is per request, so on stdio a request with modern _meta that follows an initialize on the same process is still served statelessly; mcpx does this because _meta is checked first. The gap for a request with neither is covered in the process-model register.
  • Sources. 2026-07-28/basic/versioning.mdx:176 “- A request carrying modern per-request _meta is served statelessly”; 2026-07-28/basic/versioning.mdx:182; 2026-07-28/basic/versioning.mdx:130; internal/mcpserver/conn.go:133.

LV-34 ping received from a modern client

  • What. Legacy ping (either side; the receiver MUST answer promptly with {}) is removed in 2026 in both directions: servers cannot send requests, any request proves liveness client to server, and transport keep-alives do the rest.
  • Where. 2024-11-05..2025-11-25. Absent from the 2026 schema altogether: removed, not deprecated.
  • mcpx @ 05c78b2. The server answers ping in any era (internal/mcpserver/server.go:543-544); a modern one gets {resultType: "complete"} (wire W7).
  • Value to mcpx. + low: harmless accept-liberally.
  • Effort. S — done.
  • Risk. None; docs/protocol.md:78 is the statement of intent.
  • Detail. A modern-only server answers ping with -32601 (404 on HTTP). 2026 recommends SSE comment lines as keep-alive on long streams (transports.md TR-24).
  • Sources. 2024-11-05/basic/utilities/ping.mdx:28 “1. The receiver MUST respond promptly with an empty response:”; 2026-07-28/changelog.mdx:20 “Remove ping, logging/setLevel, and notifications/roots/list_changed.”; seps/2575-stateless-mcp.md:569; internal/mcpserver/server.go:543; wire W7.

LV-35 ping sent by a client

  • What. A client may ping a legacy server as a liveness check; against a 2026 server the method does not exist.
  • Where. 2024-11-05..2025-11-25. lootbox’s health monitor pings every server periodically and reconnects with backoff (fork only; the deployed upstream build has no monitor).
  • mcpx @ 05c78b2. Client.Ping sends ping regardless of era (internal/mcpclient/client.go:726-728), but nothing in the pool or daemon calls it. The client does answer a server’s ping in any era (internal/mcpclient/modern.go:110-115), which it once answered with method-not-found.
  • Value to mcpx. + low: a probe could catch a wedged stateful server. − pinging an idle, lazily started server keeps it alive and defeats idleTimeout; pinging a modern upstream gets -32601, a false “dead”.
  • Effort. S — gate Ping() on the legacy era if it is ever used.
  • Risk. Low while unused.
  • Detail. For a modern upstream the equivalent check is server/discover or nothing.
  • Sources. internal/mcpclient/client.go:726-728; internal/mcpclient/modern.go:110-115; .lootbox/README.md:385-388 “Lootbox automatically monitors MCP server health with periodic ping()”.

LV-36 resultType required on every 2026 result

  • What. Every 2026 result MUST carry resultType: "complete", "input_required", or an extension value advertised through capabilities. An unrecognised value MUST be treated as invalid; an absent one (legacy servers) MUST be treated as "complete".
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Stamped "complete" on modern results that lack one (internal/mcpserver/server.go:432-436, :444-456), deleted below 2026 (internal/mcpserver/revisions.go:163-168); wire W6, W7. The client reads it to spot input_required (internal/mcpclient/client.go:663-665).
  • Value to mcpx. + high: done.
  • Effort. S — done.
  • Risk. None for ordinary results.
  • Detail. The TypeScript type is open ("complete" | "input_required" | string) while the prose makes unknown values invalid. Error responses carry no resultType, correctly. The task-creation result stamped "complete" where the tasks extension says "task" is in the tasks register.
  • Also recorded from the content types register. Clients treat an absent value as "complete" and an unrecognised one as invalid. The server stamps complete for modern peers (internal/mcpserver/server.go:432-436) and downgrade() deletes the field for anyone below 2026-07-28 (internal/mcpserver/revisions.go:163-168). One of the two branches that fire in production (CT-12). The deletion happens on a copy (internal/mcpserver/revisions.go:139-145), so a stored task result handed out twice is not mutated.
  • Sources. 2026-07-28/basic/index.mdx:73 “The result MUST include a resultType field”; 2026-07-28/basic/index.mdx:84; schema/2026-07-28/schema.ts:234 “resultType: ResultType;”; schema/2026-07-28/schema.ts:216; internal/mcpserver/server.go:452-454.; 2026-07-28/basic/index.mdx:85 “clients MUST treat an absent resultType as \"complete\".”; internal/mcpserver/server.go:439 “resp.Result = downgrade(resp.Result, peer.Version)”

LV-37 Request-id uniqueness: per session → among in-flight

  • What. Legacy: a request id MUST NOT have been used before by the requestor within the same session. 2026: an id MUST NOT match any other request the sender has issued and not yet had answered.
  • Where. Session-scoped in 2024-11-05..2025-11-25; in-flight-scoped in 2026-07-28.
  • mcpx @ 05c78b2. Server-initiated ids are negative so they cannot collide with a client’s (internal/mcpserver/conn.go:239-244).
  • Value to mcpx. + low: a pooled proxy may reuse ids after completion under 2026, never under legacy.
  • Effort. S.
  • Risk. None.
  • Detail. subscriptionId is the listen request’s id, so reusing an id while a listen stream is open is forbidden anyway (it is still in flight). MRTR retries MUST use a new id. Ids MUST NOT be null in every revision.
  • Sources. 2024-11-05/basic/messages.mdx:28 “- The request ID MUST NOT have been previously used by the requestor within the same”; 2026-07-28/basic/index.mdx:48; seps/2567-sessionless-mcp.md:168; internal/mcpserver/conn.go:240.

LV-38 stdio server exits promptly on stdin EOF

  • What. 2026 makes stdin EOF the primary portable shutdown signal: a server SHOULD exit promptly when its standard input is closed.
  • Where. 2026-07-28 stdio page (legacy revisions describe only the client’s side, LV-39).
  • mcpx @ 05c78b2. mcpx serve exited at stdin EOF in every recorded stdio run; the read loop ends when the scanner does (internal/mcpserver/server.go:1000-1073).
  • Value to mcpx. + med: hosts that only close the pipe get a clean exit.
  • Effort. S — done.
  • Risk. None.
  • Detail. 2026 also gives Windows equivalents for the signals.
  • Sources. 2026-07-28/basic/transports/stdio.mdx:102 “Servers SHOULD exit promptly when their standard input is closed or reads”; internal/mcpserver/server.go:1000-1073; wire S1–S5.

LV-39 Client stdio shutdown: close stdin, wait, SIGTERM, SIGKILL

  • What. The client SHOULD close the server’s stdin, wait for it to exit, send SIGTERM if it does not exit in reasonable time, then SIGKILL.
  • Where. Legacy lifecycle pages; the 2026 stdio page keeps the sequence. opencode v2 follows it (close stdin, wait 2 s, SIGTERM, SIGKILL after 2 s, whole process group). opencode v1 instead SIGTERMs descendants found with pgrep -P and closes the client, and only on instance disposal.
  • mcpx @ 05c78b2. Closes stdin and immediately SIGTERMs the process group, then SIGKILLs after plumbing.stdioDrainGrace (3 s) (internal/mcpclient/stdio.go:197-209; internal/defaults/defaults.json:40).
  • Value to mcpx. + low: servers that flush state on EOF get their chance.
  • Effort. S — wait for exit (bounded by the grace setting) before SIGTERM.
  • Risk. A server that saves on stdin EOF may be killed mid-write.
  • Detail. Using the process group means grandchildren die too, which matters for servers that fork a browser (internal/mcpclient/stdio.go:69).
  • Sources. 2024-11-05/basic/lifecycle.mdx:178 “1. First, closing the input stream to the child process (the server)”; 2025-11-25/basic/lifecycle.mdx:233-236; internal/mcpclient/stdio.go:202; v2:packages/core/src/mcp/stdio.ts:10; v2:packages/core/src/mcp/stdio.ts:57; v1:packages/opencode/src/mcp/index.ts:541; v1:packages/opencode/src/mcp/index.ts:546.

LV-40 Restart a stdio server that exited unexpectedly

  • What. 2026: if the server process exits unexpectedly the client SHOULD restart it; in-flight requests are lost and listen streams are re-established. Statelessness makes the restart free; a legacy server needs a fresh initialize.
  • Where. 2026-07-28. lootbox’s fork reconnects with exponential backoff behind a circuit breaker. opencode v1 and v2 never respawn: the server is marked failed (“Connection closed”), its tools disappear, and recovery is a manual reconnect (v2 also reconnects when that server’s integration credential changes).
  • mcpx @ 05c78b2. Dead instances are reaped (internal/pool/pool.go:344-356) and the next call starts a fresh one, which probes and initializes again; a failed start sets a cooldown that grows by plumbing.restartBackoffStep (2 s) up to plumbing.restartBackoffMax (30 s) (internal/pool/pool.go:205-208; internal/defaults/defaults.json:49-50).
  • Value to mcpx. + med: as a client this works. As a server it matters the other way round: if mcpx serve dies, opencode loses mcpx until the user reconnects, so the stdio side should outlive daemon restarts.
  • Effort. S.
  • Risk. A daemon upgrade that kills mcpx serve silently removes mcpx from every open opencode session.
  • Detail. The cap is triggered by an inline 30*time.Second rather than by the setting (internal/pool/pool.go:206): the computed backoff is replaced by restartBackoffMax only once it exceeds 30 s, so a restartBackoffMax below 30 s is not a cap.
  • Sources. 2026-07-28/basic/transports/stdio.mdx:111 “If the server process exits unexpectedly, the client SHOULD restart it.”; internal/pool/pool.go:344-356; internal/pool/pool.go:205-208; v1:packages/opencode/src/mcp/index.ts:448; v2:packages/core/src/mcp/index.ts:355; .lootbox/README.md:385-388.

Logging

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  logging/setLevel, notifications/message, log levels and the 2026 per-request logLevel, across revisions and products

Logging is the server-to-client notifications/message stream and the knobs that control it. 2026-07-28 changes it more than its size suggests: it removes logging/setLevel, moves the level into each request’s _meta, flips the default from “the server decides” to “send nothing”, and deprecates the whole feature in the same revision. For mcpx the two things that matter are that its client still sends the removed setLevel to modern upstreams, and that its own code comment and protocol doc describe the 2026 rule backwards. Request-scoped delivery of notifications/message (never on a listen stream) is one rule shared with progress and is registered once, in notifications.md.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
LOG-01logging/setLevel request removed2024-11-05 has 2026-07-28 removes has better replacement24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —acc. — answers {} in every era, does nothing+ lowSlow
LOG-02Per-request _meta io.modelcontextprotocol/logLevel2026-07-28 has 2026-07-28 deprecates mcpx missing specs conflict26-07 ✓ (dep.)✗ — never sent upstream, never read from hosts (#200)+ lowSlow
LOG-03No logLevel means no log messages at all2026-07-28 has specs conflict24-11..25-11: server decides · 26-07: MUST NOT emit✓ — emits none; code comment states the opposite+ lowSmed
LOG-04Invalid log level rejects the whole request2026-07-28 has24-11..25-11: setLevel -32602 · 26-07: any request -32602partial — setLevel never validates; _meta level ignored+ lowSlow
LOG-05RFC 5424 log levels unchanged2024-11-05 has 2026-07-28 deprecates24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep.n/a — emits none; client only ever asks info− mootSlow
LOG-06Logging feature deprecated (SEP-2577)2026-07-28 deprecates26-07 dep.; removable from 2027-07-28✓ — nothing declared or emitted− deprecatedSlow
LOG-07mcpx client sends setLevel info to every logging upstream2026-07-28 removes mcpx missingmcpx only; opencode v1/v2 never send setLevel✗ — removed method sent to modern upstreams; level hard-coded (#200)+ medSlow
LOG-08Relaying upstream log messages to an MCP hostmcpx missing 2026-07-28 deprecatesspec: any server may emit; mcpx: event bus only✓ — relayed to a host that set a level, during its calls (#212; docs/protocol.md §4.4)+ lowMlow
LOG-09Client handling of notifications/messageopencode v1 has has no replacementopencode v1 ✓ · opencode v2 ✗n/a — mcpx sends none to hosts− mootSlow

LOG-01 logging/setLevel request removed

  • What. The session-wide “send me logs at this level and above” request is gone in 2026-07-28; the level now travels on each request (LOG-02).
  • Where. 2024-11-05 through 2025-11-25; removed in 2026-07-28. opencode v1 and v2 never send it (v1:packages/opencode/src/mcp/index.ts:164; v2:packages/core/src/mcp/client.ts:91).
  • mcpx @ 05c78b2. Accepted from every era and answered {} (plus resultType for a 2026 caller), with no effect and nothing forwarded upstream (internal/mcpserver/server.go:565-572). Wire W25: a 2026 logging/setLevel gets {"resultType":"complete"}.
  • Value to mcpx. + low: accept-liberally is the right call; the handler’s comment says refusing “would make a well-behaved client that asked anyway treat the whole connection as degraded”.
  • Effort. S — done.
  • Risk. None as is.
  • Detail. Params are {level: LoggingLevel}. The legacy rule was “all logs at this level and higher”. mcpx answers without parsing the params, so an invalid level is not rejected (LOG-04). The FeatLoggingSetLevel comment that justifies the removal is wrong about the replacement (LOG-03).
  • Sources. schema/2025-11-25/schema.ts:1520 “logging/setLevel”; 2026-07-28/changelog.mdx:20 “Remove”; schema/2024-11-05/schema.ts:714 “The server should send all logs at this level and higher”; internal/mcpserver/server.go:572 “return reply(map[string]any{})”; wire W25.

LOG-02 Per-request _meta io.modelcontextprotocol/logLevel

  • What. An optional LoggingLevel in a request’s _meta opts that one request into notifications/message at or above the level. It is the replacement for setLevel, and it is itself deprecated along with the rest of Logging.
  • Where. 2026-07-28 only; listed in the reserved _meta key table.
  • mcpx @ 05c78b2. Never sent: no logLevel anywhere in internal/mcpclient, so a 2026 upstream can never send mcpx a log message. As a server mcpx ignores it on host requests (it emits nothing anyway).
  • Value to mcpx. + low: if upstream logs are wanted in the event bus from modern servers, the level has to ride on every forwarded request. Deprecated, so not worth more than that.
  • Effort. S — one _meta key in withMeta, fed from the per-server logLevel setting that already exists (LOG-07).
  • Risk. Without it, upstream diagnostics from 2026 servers are lost.
  • Detail. There is no way to set a minimum once; a proxy that wants logs must add the key to each forwarded request. A proxy relaying a host’s own level should forward exactly what the host sent, because adding a level the host did not ask for makes the upstream emit what the host did not request (LOG-03). The key is simultaneously the replacement for setLevel and @deprecated in the same schema.
  • Also recorded from the meta register. The server emits no notifications/message at all, so it is compliant by omission (internal/mcpserver/server.go:565-572), but the comment at internal/mcpserver/revisions.go:72-73 says setLevel was removed “in favour of servers emitting at a level of their own choosing”, the opposite of the spec. The client never sets the key upstream, so a 2026 upstream never sends mcpx logs; what it sends instead is in the logging register. Being the replacement and deprecated at once is why this row carries specs conflict. Logging behaviour is in the logging register.
  • Sources. schema/2026-07-28/schema.ts:110 “"io.modelcontextprotocol/logLevel"?: LoggingLevel;”; schema/2026-07-28/schema.ts:106 “@deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).”; 2026-07-28/basic/index.mdx:356 “Minimum log level the server should emit for a request”; https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging.; 2026-07-28/server/utilities/logging.mdx:63; 2026-07-28/server/utilities/logging.mdx:100; internal/mcpserver/revisions.go:73 “// favour of servers emitting at a level of their own choosing.”.

LOG-03 No logLevel means no log messages at all

  • What. Legacy: if the client never sent setLevel, the server MAY decide which messages to send. 2026: if a request carries no logLevel, the server MUST NOT send notifications/message for it. The default flips from “server decides” to “none”.
  • Where. Legacy default in 2024-11-05..2025-11-25; the inverse in 2026-07-28.
  • mcpx @ 05c78b2. Compliant by omission: it emits no notifications/message (internal/mcpserver/server.go:566-568). But the feature table comment says 2026 removed setLevel “in favour of servers emitting at a level of their own choosing” (internal/mcpserver/revisions.go:72-73), which is the legacy default presented as the 2026 one.
  • Value to mcpx. + low: fix the comment before someone builds on it.
  • Effort. S — two comment lines.
  • Risk. med: the wrong model leads straight to forwarding unsolicited upstream logs to 2026 hosts, which violates a MUST.
  • Detail. This is a behaviour inversion, not just a method removal. A dual-era proxy must drop legacy upstream log messages for a 2026 host unless that host’s request carried logLevel. The schema says it twice: the _meta doc (“If absent, the server MUST NOT send any”) and the logging page.
  • Sources. schema/2025-11-25/schema.ts:1545 “the server MAY decide which messages to send automatically”; 2026-07-28/server/utilities/logging.mdx:63 “The server MUST NOT”; schema/2026-07-28/schema.ts:102 “If absent, the server MUST NOT send any”; internal/mcpserver/revisions.go:73 “favour of servers emitting at a level of their own choosing.”.

LOG-04 Invalid log level rejects the whole request

  • What. Legacy: an unknown level in logging/setLevel gets -32602. 2026: an unknown logLevel in a request’s _meta SHOULD fail that whole request with -32602, whatever the request was.
  • Where. -32602 on setLevel in 2024-11-05..2025-11-25; on any request in 2026-07-28.
  • mcpx @ 05c78b2. logging/setLevel returns {} without reading its params (internal/mcpserver/server.go:565-572), so a bad level is accepted. A 2026 _meta level is not read at all, so it cannot fail a request.
  • Value to mcpx. + low: as a proxy, the useful rule is the reverse one: never forward a level the upstream will reject, because that fails an unrelated tool call.
  • Effort. S.
  • Risk. A typo in a logging hint failing a tool call, if mcpx ever forwards levels unchecked.
  • Detail. SHOULD, so a server may also ignore the bad value. Validation belongs where mcpx adds or forwards the key (LOG-02), against the eight RFC 5424 names (LOG-05).
  • Sources. 2024-11-05/server/utilities/logging.mdx:110 “Invalid log level”; 2026-07-28/server/utilities/logging.mdx:100 “value carried in a request’s”; 2026-07-28/server/utilities/logging.mdx:101 “the server SHOULD reject that”.

LOG-05 RFC 5424 log levels unchanged

  • What. debug, info, notice, warning, error, critical, alert, emergency, identical in all five revisions. The LoggingLevel type is marked deprecated in 2026.
  • Where. All five.
  • mcpx @ 05c78b2. Emits none. As a client it only ever asks for info (internal/daemon/hooks.go:29, LOG-07).
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. The notifications/message params are level, optional logger, and data (any JSON), unchanged in every revision.
  • Sources. schema/2024-11-05/schema.ts:747 “export type LoggingLevel =”; schema/2026-07-28/schema.ts:2075 “export type LoggingLevel =”; schema/2024-11-05/schema.ts:733 “logger?: string;”; schema/2026-07-28/schema.ts:2043 “data: unknown;”.

LOG-06 Logging feature deprecated (SEP-2577)

  • What. The whole feature (capability, notification, level type, _meta key) is deprecated in 2026-07-28. New implementations SHOULD NOT adopt it; existing ones SHOULD move to stderr (stdio) or OpenTelemetry.
  • Where. 2026-07-28; eligible for removal no earlier than twelve months after the revision.
  • mcpx @ 05c78b2. Declares and emits nothing (CAP-12), so there is nothing to retire.
  • Value to mcpx. − deprecated: nothing to build.
  • Effort. S.
  • Risk. None.
  • Detail. Deprecated is not removed: a 2026 server may still emit when asked (LOG-02). Roots and Sampling are deprecated by the same SEP; Elicitation is not.
  • Sources. 2026-07-28/server/utilities/logging.mdx:8 “The Logging feature is deprecated as of protocol version”; 2026-07-28/server/utilities/logging.mdx:13 “New implementations”; 2026-07-28/deprecated.mdx:28 “Logging”.

LOG-07 mcpx client sends setLevel info to every logging upstream

  • What. After connecting, the pool sends logging/setLevel {"level":"info"} to any upstream that declares logging, including a modern one, for which the method no longer exists. It should use the per-request _meta key for modern upstreams.
  • Where. mcpx only. opencode v1 and v2 never send setLevel (v1:packages/opencode/src/mcp/index.ts:164; v2:packages/core/src/mcp/client.ts:91).
  • mcpx @ 05c78b2. LogLevel: "info" is an inline constant (internal/daemon/hooks.go:29); the pool sends it (internal/pool/pool.go:410-417); the client checks only Supports("logging"), not the era (internal/mcpclient/client.go:568-575). A per-server logLevel is resolved in config and read by nothing (internal/config/config.go:483).
  • Value to mcpx. + med: correct 2026 behaviour, and a configurable level instead of a hard-coded one.
  • Effort. S — era switch plus wiring the existing setting.
  • Risk. low: a modern upstream answers -32601, which is swallowed (_ =); the cost is that logs from modern upstreams never flow.
  • Detail. This is a send-conservatively conflict (a method sent to a peer whose revision does not define it). The hard-coded "info" breaks the project’s “nothing hardcoded” rule. Legacy upstreams that do not declare logging are correctly left alone.
  • Sources. internal/daemon/hooks.go:29; internal/mcpclient/client.go:568-575; internal/config/config.go:483; schema/2026-07-28/schema.ts:110 “"io.modelcontextprotocol/logLevel"?: LoggingLevel;”.

LOG-08 Relaying upstream log messages to an MCP host

  • What. Upstream notifications/message is published on mcpx’s event bus as a ServerLog event (visible on /v1/events and in the TUI). The MCP notifier forwards only list_changed and resources/updated, so no MCP host ever sees an upstream log.
  • Where. Every revision defines notifications/message (deprecated in 2026). mcpx: event bus only.
  • mcpx @ 05c78b2. internal/daemon/hooks.go:31-33 publishes the event; the listen filter kinds are only the four list and resource kinds (internal/cli/serve.go:726-742); MCPNotification has no ServerLog case (internal/events/events.go:252-271).
  • Value to mcpx. + low: hosts that show server logs could; for an agent host, logs in context are mostly noise, and the event bus is the better home.
  • Effort. M — request-scoped forwarding on the call’s own stream, only when the host asked (legacy setLevel, 2026 _meta logLevel).
  • Risk. low. Blanket forwarding would itself break LOG-03’s MUST and the request-scoping rule in notifications.md.
  • Detail. A relayed message would have to be attributed to the request that caused it, which on a shared upstream instance mcpx cannot always do (the same limit as elicitation on shared instances, see elicitation.md). The 2026 page also says log messages MUST NOT contain credentials or secrets; a relay inherits that obligation.
  • Sources. internal/daemon/hooks.go:32 “r.publish(events.Event{Kind: events.ServerLog, Server: server, Data: mustJSON(m)})”; internal/cli/serve.go:726-742; internal/events/events.go:252-271; 2026-07-28/server/utilities/logging.mdx:123 “Log messages MUST NOT contain:”.

LOG-09 Client handling of notifications/message

  • What. opencode v1 maps server log messages onto its own logger (“MCP server log”, by level). opencode v2 registers no logging handler, so server logs are dropped.
  • Where. opencode v1 only; v2 removed it with no replacement. Neither version sends setLevel.
  • mcpx @ 05c78b2. Sends no notifications/message to hosts and no longer declares logging (internal/mcpserver/server.go:565), so neither opencode version loses anything from mcpx.
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. v1 maps logger, level and data into structured fields. Because neither version sends setLevel, a legacy server talking to v1 sends whatever it decides (LOG-03’s legacy default).
  • Sources. v1:packages/opencode/src/mcp/index.ts:457; v1:packages/opencode/src/mcp/index.ts:474; v2:packages/core/src/mcp/client.ts:140.

_meta

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  The _meta extension point across revisions: naming and reservation rules, reserved keys, and which keys mcpx reads, writes, forwards or drops.

_meta is the reserved metadata object on requests, notifications and results. It has existed since 2024-11-05, acquired naming rules in 2025-06-18, had its reserved-prefix rule inverted in 2025-11-25, and in 2026-07-28 became the carrier for protocol state itself: version, capabilities, identity, log level and subscription ids. For mcpx, which terminates one MCP conversation and starts another, the question is which keys belong to a single hop and which must travel end to end. It replaces the per-hop keys correctly, but drops everything else in both directions, including trace context and opencode v2’s per-session isolation hint. The per-request version, capability, client and server identity keys are rows in lifecycle-versioning.md (LV-19..LV-22).

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
META-01_meta on requests, notifications and results, without rules2024-11-05 has24-11 ✓(schema) · 25-03 ✓(schema) · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — client keeps caller keys when adding its own+ lowSlow
META-02Key-name format and forward-DNS prefix reservation2025-06-18 has25-06 ✓n/a — mcpx defines no _meta keys+ lowSlow
META-03Reservation flips to reverse-DNS “second label”2025-11-25 has specs conflict25-11 ✓ · 26-07 ✓n/a — no mcpx keys yet+ lowSlow
META-04Typed _meta objects per message kind2026-07-28 has26-07 ✓partial — request keys read; notification and result keys never set+ medSlow
META-05RequestParams._meta required, so params is required2026-07-28 has26-07 ✓✓ — client creates params; server treats absence as legacy+ lowSlow
META-062026 reserved-key table2026-07-28 has specs conflict26-07 ✓partial — three request keys handled; the rest ignored+ medSmed
META-07traceparent / tracestate / baggage2026-07-28 has mcpx missing26-07 ✓ (prose only)✓ — passed upstream from a host’s tools/call (#212; docs/protocol.md §4.4)+ medSlow
META-08clientInfo / serverInfo are self-reported, display-only2026-07-28 has26-07 ✓ (legacy silent)✓ — neither drives behaviour+ lowSlow
META-09ai.opencode/sessionID on every opencode v2 tools/callopencode v2 has mcpx missing has better replacementopencode v2 ✓ · opencode v1 —✗ — ignored; every host shares one scope (#211)+ highMhigh
META-10Per-hop versus end-to-end keys through a proxy2026-07-28 has26-07 (implied)partial — per-hop keys replaced; nothing end-to-end survives+ medMmed
META-11Upstream result _meta reaching the host2024-11-05 has mcpx missing24-11 ✓(schema) · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — lost via mcpx_call; kept on /v1 and in scripts (#206)+ medMlow

META-01 _meta on requests, notifications and results, without rules

  • What. 2024-11-05 already reserves params._meta on requests (typed with the single key progressToken), params._meta on notifications and result._meta on results (both open maps). No key-format or reservation rules exist until 2025-06-18.
  • Where. Schema-only in 2024-11-05 and 2025-03-26; prose rules from 2025-06-18.
  • mcpx @ 05c78b2. When the client adds its own 2026 keys it keeps any the caller already set (internal/mcpclient/modern.go:73-81).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. Progress opt-in has always lived in _meta, never as a top-level parameter (PC-04).
  • Sources. schema/2024-11-05/schema.ts:21 “_meta?: {”; schema/2024-11-05/schema.ts:25 “progressToken?: ProgressToken;”; schema/2024-11-05/schema.ts:37; schema/2024-11-05/schema.ts:46 “_meta?: { [key: string]: unknown };”; internal/mcpclient/modern.go:73-81.

META-02 Key-name format and forward-DNS prefix reservation

  • What. 2025-06-18 defines a key as an optional prefix (dot-separated labels, then /) plus a name, and reserves any prefix of the form “zero or more labels, then modelcontextprotocol or mcp, then any label”, for example modelcontextprotocol.io/, mcp.dev/, api.modelcontextprotocol.org/, tools.mcp.com/. Implementations MUST NOT make assumptions about values at reserved keys. The same revision adds _meta to more types (PR #710).
  • Where. 2025-06-18; replaced one revision later (META-03).
  • mcpx @ 05c78b2. Defines no _meta keys of its own (internal/mcpclient/modern.go:12-16 lists only the three io.modelcontextprotocol/ request keys).
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. None.
  • Detail. Names, unless empty, begin and end with an alphanumeric and may contain hyphens, underscores and dots. The _meta fields added to Tool, Resource, ResourceTemplate, Prompt, Root and content blocks are covered in the tools, resources, prompts, roots and content-types registers.
  • Sources. 2025-06-18/basic/index.mdx:140 “- Any prefix beginning with zero or more valid labels, followed by modelcontextprotocol or mcp, followed by any valid label,”; 2025-06-18/basic/index.mdx:142; 2025-06-18/changelog.mdx:35 “1. Add _meta field to additional interface types”.

META-03 Reservation flips to reverse-DNS “second label”

  • What. 2025-11-25 says implementations SHOULD use reverse-DNS prefixes and reserves a prefix exactly when its second label is modelcontextprotocol or mcp (io.modelcontextprotocol/, dev.mcp/, org.modelcontextprotocol.api/, com.mcp.tools/); com.example.mcp/ is not reserved. So tools.mcp.com/ was reserved under 2025-06-18 and is not under 2025-11-25, while com.mcp.tools/ now is.
  • Where. 2025-11-25 and 2026-07-28 (same text). The 2025-11-25 changelog does not mention the change.
  • mcpx @ 05c78b2. No mcpx keys yet (internal/mcpclient/modern.go:12-16).
  • Value to mcpx. + low: any key mcpx adds should use a reverse-DNS prefix whose second label is not mcp or modelcontextprotocol; dev.mcpx/ qualifies under both rules.
  • Effort. S.
  • Risk. None.
  • Detail. opencode’s ai.opencode/ is reverse DNS for opencode.ai and unreserved under both rules.
  • Sources. 2025-11-25/basic/index.mdx:207 “- Implementations SHOULD use reverse DNS notation”; 2025-11-25/basic/index.mdx:208 “- Any prefix where the second label is modelcontextprotocol or mcp is reserved for MCP use.”; 2026-07-28/basic/index.mdx:337; 2026-07-28/basic/index.mdx:339.

META-04 Typed _meta objects per message kind

  • What. The 2026 schema types _meta per kind: MetaObject (the naming rules), RequestMetaObject (progressToken, required protocolVersion, clientInfo, required clientCapabilities, logLevel), NotificationMetaObject (subscriptionId), ResultMetaObject (serverInfo), and SubscriptionsListenResultMetaObject, where subscriptionId is required. It fixes which key is legal where: version and capabilities only on requests, subscriptionId on notifications and the listen result, serverInfo on results. The changelog does not mention these types.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Reads the two required request keys (internal/mcpserver/conn.go:171-175); sets no notification key (SUB-05) and no result key (lifecycle-versioning.md LV-22).
  • Value to mcpx. + med: the typed objects are the checklist for what a 2026 peer expects on each message.
  • Effort. S.
  • Risk. Low.
  • Detail. 2026’s embedded input requests are not JSONRPCRequests: ListRootsRequest.params._meta is a plain MetaObject without version or capabilities, and CreateMessageRequest has no _meta; see the roots and sampling registers.
  • Sources. schema/2026-07-28/schema.ts:54 “export type MetaObject = Record<string, unknown>;”; schema/2026-07-28/schema.ts:63; schema/2026-07-28/schema.ts:120; schema/2026-07-28/schema.ts:143; schema/2026-07-28/schema.ts:1326; internal/mcpserver/conn.go:171-175.

META-05 RequestParams._meta required, so params is required

  • What. 2026 declares RequestParams { _meta: RequestMetaObject } with _meta non-optional, and concrete requests type params as required (PaginatedRequest.params, DiscoverRequest.params). In legacy revisions params was optional for list requests and ping.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The client’s withMeta creates params when there is none (internal/mcpclient/modern.go:66-91). The server cannot see a version in a request without params, so it serves it as legacy (lifecycle-versioning.md LV-24).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. The base Request.params stays optional in the schema, so a request without params is valid as a JSONRPCRequest yet malformed by the prose.
  • Sources. schema/2026-07-28/schema.ts:180 “_meta: RequestMetaObject;”; schema/2026-07-28/schema.ts:1064 “params: PaginatedRequestParams;”; schema/2025-11-25/schema.ts:637 “params?: PaginatedRequestParams;”; schema/2026-07-28/schema.ts:188; internal/mcpclient/modern.go:66-91.

META-06 2026 reserved-key table

  • What. 2026 lists the reserved keys: progressToken; io.modelcontextprotocol/protocolVersion, /clientInfo, /clientCapabilities, /logLevel, /subscriptionId; and traceparent, tracestate, baggage. Official extensions define more under io.modelcontextprotocol/, third-party extensions use their own vendor prefix, and implementations MUST NOT make assumptions about values at these keys.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Knows protocolVersion, clientCapabilities and clientInfo (internal/mcpserver/conn.go:171-175; internal/mcpclient/modern.go:12-16); nothing handles logLevel, subscriptionId, serverInfo or the trace keys.
  • Value to mcpx. + med: each unhandled key is a row below or in lifecycle-versioning.md.
  • Effort. S per key.
  • Risk. As in the per-key rows.
  • Detail. The table omits io.modelcontextprotocol/serverInfo, which the schema defines on ResultMetaObject and the same page describes in a separate result-fields table. SEP-2575 lists roots among the per-request fields, but the released revision defines no roots key; roots reach a server only through MRTR roots/list input requests. Per-key rows: lifecycle-versioning.md LV-19..LV-22 and PC-04..TASK-15 here.
  • Sources. 2026-07-28/basic/index.mdx:348 “The following _meta keys are reserved by this specification:”; seps/2575-stateless-mcp.md:760; 2026-07-28/basic/index.mdx:328; 2026-07-28/basic/index.mdx:358; 2026-07-28/basic/index.mdx:402; schema/2026-07-28/schema.ts:157; internal/mcpserver/conn.go:171-175.

META-07 traceparent / tracestate / baggage

  • What. Unprefixed keys reserved for W3C Trace Context and W3C Baggage (SEP-414); when present, values MUST follow those formats. The spec says the exception exists for compatibility with existing implementations and the OpenTelemetry semantic conventions for MCP.
  • Where. 2026-07-28 (SDKs used them before).
  • mcpx @ 05c78b2. No traceparent in internal/; the host’s _meta is dropped (internal/mcpserver/server.go:658-665) and never reaches the upstream request.
  • Value to mcpx. + med: a proxy is where a trace continues from agent to mcpx to upstream.
  • Effort. S to pass through; M to create spans.
  • Risk. Traces break at mcpx.
  • Detail. The keys are prose-only: the 2026 schema has no traceparent (its MetaObject is Record<string, unknown>). The prose calls them “an exception to the prefix requirement above”, but the key-format rules above make the prefix optional and the same table lists an unprefixed progressToken; the requirement is only implied by “third-party extensions use their own vendor prefix”.
  • Sources. 2026-07-28/basic/index.mdx:421 “As an exception to the prefix requirement above, the keys traceparent, tracestate, and”; 2026-07-28/basic/index.mdx:358; 2026-07-28/basic/index.mdx:330; 2026-07-28/basic/index.mdx:362; internal/mcpserver/server.go:658-665.

META-08 clientInfo / serverInfo are self-reported, display-only

  • What. Both are unverified. Implementations SHOULD NOT change behaviour or make security decisions based on them; display, logging and debugging are their purpose.
  • Where. 2026-07-28 says so explicitly; legacy revisions are silent.
  • mcpx @ 05c78b2. The server never reads clientInfo (the key is only declared, internal/mcpserver/conn.go:174), and pool scope does not key on it.
  • Value to mcpx. + low: logging it for attribution is fine; special-casing agents by name is not.
  • Effort. S.
  • Risk. None.
  • Detail. Aggregating clients should not rely on serverInfo.name being unique; see the tools register on namespacing.
  • Sources. 2026-07-28/basic/index.mdx:407 “intended for display, logging, and debugging. Implementations SHOULD NOT”; internal/mcpserver/conn.go:174.

META-09 ai.opencode/sessionID on every opencode v2 tools/call

  • What. opencode v2’s MCP client attaches the calling opencode session id as the vendor key _meta["ai.opencode/sessionID"] on every tool call, direct and code-mode child calls alike. It is advisory: a server that chooses to can partition its state by it. opencode v1 sends nothing but name and arguments.
  • Where. opencode v2 only, in exactly one place.
  • mcpx @ 05c78b2. Ignored. tools/call reads {name, arguments} (internal/mcpserver/server.go:658-665), no ai.opencode string exists in internal/, and Backend.Call(ctx, namespace, tool, args) has no caller identity (internal/mcpserver/server.go:45). Pool scope for /mcp comes from a process-wide session key that every HTTP host shares (process-model register, finding M-3).
  • Value to mcpx. + high: using it as the scope key gives opencode v2 real per-session isolation of stateful servers through /mcp, with no plugin and no shell environment; OPENCODE-V2.md:92-94 notes that nothing else provides this.
  • Effort. M — reading the key is small; threading a caller identity from tools/call into pool leasing is the work.
  • Risk. Not done: two opencode sessions on one daemon share one browser.
  • Detail. Other clients will not send it. v2 tells the server which session is calling and leaves partitioning to the server.
  • Also recorded from the code mode register. opencode v2 only; used in exactly one place. Script to daemon: x-mcpx-session headers (internal/codegen/emit.go:528-533). As a server: no ai.opencode string in internal/, and Backend.Call(ctx, namespace, tool, args) has no caller identity (internal/mcpserver/server.go:45). As a client: rg finds no _meta in internal/pool. Child calls carry it because they run the same MCP executor with sessionID: context.sessionID (v2:packages/core/src/tool/mcp.ts:69). The key is prefixed as _meta requires. The mcpx side has three gaps held in other registers: the plugin’s mcpx_exec sends no session and no cwd, so every call gets a fresh exec-<runID> session and the daemon’s directory (plugin-API register, PLG-39); session-scoped instances started through /v1/exec are never released at run end (process-model register, PM-12); and every HTTP MCP host shares one pool session key (PM-10). What mcpx gets right is that a session lease is pinned for the whole script run, so navigate, snapshot and click reach the same browser (PM-11).
  • Sources. v2:packages/core/src/mcp/client.ts:278; v2:packages/core/src/tool/mcp.ts:69 “sessionID: context.sessionID,”; v1:packages/opencode/src/mcp/catalog.ts:56; OPENCODE-V2.md:89; internal/mcpserver/server.go:45.

META-10 Per-hop versus end-to-end keys through a proxy

  • What. A proxy that terminates one MCP conversation and starts another must not forward per-hop keys (protocolVersion, clientCapabilities, clientInfo) verbatim, because forwarding a host’s capabilities upstream promises capabilities the proxy must then honour. End-to-end keys (trace context, vendor hints, a progress correlation) should cross. The spec implies this through “MUST NOT make assumptions about values at these keys”; it does not spell it out for proxies.
  • Where. 2026-07-28 (implied).
  • mcpx @ 05c78b2. The host’s _meta is deleted before dispatch (internal/mcpserver/ask.go:95-108) and the upstream request gets mcpx’s own keys (internal/mcpclient/modern.go:60-91), so per-hop keys are right. Nothing end-to-end survives (PC-04, META-07, META-09). withMeta keeps any key the caller already set (internal/mcpclient/modern.go:73-81), so passing a host’s _meta through unfiltered would let the host’s clientCapabilities override mcpx’s.
  • Value to mcpx. + med: one allowlist decides what crosses.
  • Effort. M.
  • Risk. Forwarding everything breaks send-conservatively; forwarding nothing breaks tracing and isolation.
  • Detail. None.
  • Sources. 2026-07-28/basic/index.mdx:328 “implementations MUST NOT make assumptions about values at these keys.”; internal/mcpserver/ask.go:100; internal/mcpclient/modern.go:73-81.

META-11 Upstream result _meta reaching the host

  • What. Results carry _meta (an open map since 2024-11-05, formalised in 2025-06-18); extensions such as MCP Apps put UI resource pointers there.
  • Where. Every revision.
  • mcpx @ 05c78b2. mcpx_call renders the upstream result as text, decoding only content, structuredContent and isError (internal/cli/commands.go:385-415), so result _meta is lost. /v1/call passes the raw result (internal/daemon/server.go:703), and the script client exposes it as .raw (internal/codegen/emit.go:476-491).
  • Value to mcpx. + med: extensions that ride in _meta work through mcpx.
  • Effort. M.
  • Risk. Low today.
  • Detail. None.
  • Sources. schema/2024-11-05/schema.ts:46; internal/cli/commands.go:386-393; internal/daemon/server.go:703; internal/codegen/emit.go:476-491.

Multi round-trip requests (the 2026 input_required mechanism)

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  InputRequiredResult, inputRequests, requestState and retries in 2026-07-28, and how mcpx and opencode v2 implement them

In 2026-07-28 a server can no longer send the client a request: when it needs the user’s answer, a model completion or the client’s roots, it answers the client’s own tools/call, prompts/get or resources/read early with an InputRequiredResult, and the client sends the same request again with the answers and an opaque requestState. This page covers that mechanism for elicitation, sampling and roots alike; the embedded request shapes are in elicitation.md, sampling.md and roots.md, and the resultType field every 2026 result carries is registered with lifecycle. For mcpx the point is that its requestState is a signed token bound to the Mcp-Session-Id that 2026 removed, so the mechanism works only for 2026 hosts that behave like legacy ones (MRTR-08), and that results routed through it come back flattened to text (MRTR-19).

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
MRTR-01InputRequiredResult with resultType: "input_required"2026-07-28 has26-07 only✓ — produced for 2026 hosts and consumed from upstreams+ highSlow
MRTR-02inputRequests: server-assigned keys, bare {method, params} values2026-07-28 has26-07 only✓ — keys are broker question ids+ medSlow
MRTR-03Only tools/call, prompts/get, resources/read may answer input_required2026-07-28 has specs conflict26-07 only✓ — exactly these three are interruptible+ lowSlow
MRTR-04Server-initiated requests removed (ServerRequest union gone)2026-07-28 removes has better replacement24-11..25-11 ✓ · 26-07 —✓ — wire requests for legacy, MRTR for 2026+ highSlow
MRTR-05No input request of a kind the client did not declare2026-07-28 has26-07 only✓ — only sendable questions included+ medSlow
MRTR-06requestState opaque; client echoes it exactly, never invents one2026-07-28 has26-07 only✓ — echoed verbatim; stale fields removed per retry+ medSlow
MRTR-07Server MUST integrity-protect requestState2026-07-28 has26-07 only✓ — base64url(payload).HMAC-SHA256, per-process key+ highSlow
MRTR-08requestState SHOULD bind the authenticated principal2026-07-28 has mcpx missing26-07 only✗ — bound to Mcp-Session-Id, which 2026 removed (#201)+ highMhigh
MRTR-09requestState SHOULD carry a short expiry2026-07-28 has26-07 only✓ — e expiry, proto.stateTTL 30 m+ lowSlow
MRTR-10requestState SHOULD identify method and parameters digest2026-07-28 has mcpx missing26-07 only✗ — call id only; retry params ignored (#201)+ medSmed
MRTR-11Single use needs server-side enforcement2026-07-28 has26-07 onlyn/a — not enforced; replay re-polls the same call− mootSlow
MRTR-12Retry carries inputResponses keyed like inputRequests2026-07-28 has26-07 only✓ — built per key; relayed per key+ medSlow
MRTR-13Retry uses a new JSON-RPC id; state only for that retry2026-07-28 has26-07 only✓ — each round takes a fresh id+ lowSlow
MRTR-14requestState-only result: client MAY retry at once2026-07-28 has26-07 onlypartial — retries immediately, no backoff+ lowSmed
MRTR-15Rounds are unbounded on the server side2026-07-28 has26-07 onlypartial — mcpx as server abandons after 8 rounds+ lowSlow
MRTR-16Client-side cap on rounds2026-07-28 has opencode v2 hasspec: none; mcpx 8; opencode v2 10✓ — elicit.inputRounds 8, then an error+ lowSlow
MRTR-17No per-input error channel2026-07-28 has specs conflict has no replacement26-07 onlypartial — one failed input aborts the whole call+ lowMlow
MRTR-18Client fulfils input_required and retries2026-07-28 has opencode v2 hasmcpx ✓; opencode v2 SDK ✓; opencode v1, lootbox ✗partial — implemented; unreachable until discovery is fixed+ highSmed
MRTR-19Results returned through MRTR keep their full shapemcpx missingspec: result is the method’s own; mcpx: text✗ — flattened to one text block (#201)+ medMmed
MRTR-20Where the interrupted call’s state lives2026-07-28 hasspec: in requestState; mcpx: daemon taskpartial — call runs as a daemon task; token names it+ medLmed

MRTR-01 InputRequiredResult with resultType: "input_required"

  • What. An interim result carrying optional inputRequests and optional requestState; at least one MUST be present. It terminates the original request; the retry is a new, independent request.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. Produced for 2026 hosts by inputRequired (internal/mcpserver/ask.go:256-266) and detected on upstream results by the client (internal/mcpclient/client.go:663-676).
  • Value to mcpx. + high: the only way a 2026 host can be asked anything.
  • Effort. S — done.
  • Risk. None beyond the rows below.
  • Detail. The server builds {"resultType":"input_required","inputRequests":{…},"requestState":"…"}; wire M2 shows one. Everything a server needs to resume should be in the retry itself (MRTR-20 on how far mcpx is from that).
  • Sources. schema/2026-07-28/schema.ts:584 “export interface InputRequiredResult extends Result {”; schema/2026-07-28/schema.ts:575 “At least one of”; internal/mcpserver/ask.go:256-266; internal/mcpclient/client.go:663-676; wire M2; https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr.

MRTR-02 inputRequests: server-assigned keys, bare {method, params} values

  • What. A map from server-chosen keys, unique within the request, to ElicitRequest, CreateMessageRequest or ListRootsRequest objects. There is no jsonrpc and no id; the key does the id’s job.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Keys are the broker’s question ids (elc-…) and values are the upstream’s method and raw params (internal/mcpserver/ask.go:258-259).
  • Value to mcpx. + med: done.
  • Effort. S.
  • Risk. None.
  • Detail. Because the params are copied raw, the originator prefix and the elicitationId strip that the legacy path applies are missing here (see elicitation.md, ELI-10 and ELI-18). Keys need only be unique per request; the tasks extension requires uniqueness over a task’s lifetime (tasks.md).
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:228 “keys are server assigned identifiers”; schema/2026-07-28/schema.ts:538 “CreateMessageRequest | ListRootsRequest | ElicitRequest;”; internal/mcpserver/ask.go:259.

MRTR-03 Only tools/call, prompts/get, resources/read may answer input_required

  • What. The prose lists three methods and says servers MUST NOT send InputRequiredResult on any other request. A schema comment on InputResponseRequestParams says the params “may be included in any client-initiated request”.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Exactly these three can block on the client (internal/mcpserver/ask.go:318-325).
  • Value to mcpx. + low: aligned.
  • Effort. S.
  • Risk. None.
  • Detail. The schema’s actual extends wire InputResponseRequestParams into those three param types only, agreeing with the prose; the comment is the outlier. Completion is not on the list, so completion/complete can never ask.
  • Also recorded from the resources register. resources/list and resources/templates/list cannot. The server routes resources/read through the ask path (internal/mcpserver/server.go:701-705), and renders the final answer as one text item (internal/mcpserver/ask.go:276-287). An InputRequiredResult answer is not a cacheable result (pagination-caching register). The mechanism and its gaps are in the MRTR register.
  • Also recorded from the prompts register. The server routes prompts/get through the ask path (internal/mcpserver/server.go:742-746); the finished call is rendered as one user text message (internal/mcpserver/ask.go:271-275). GetPromptResult is not a cacheable result, unlike ReadResourceResult (pagination-caching register). The mechanism is in the MRTR register.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:192 “Servers MUST NOT send”; schema/2026-07-28/schema.ts:598 “These parameters may be included in any client-initiated request.”; internal/mcpserver/ask.go:318-325.; schema/2026-07-28/schema.ts:1205 “export interface ReadResourceRequestParams”; schema/2026-07-28/schema.ts:1245 “result: ReadResourceResult | InputRequiredResult;”; internal/mcpserver/server.go:701-705; internal/mcpserver/ask.go:276-287; schema/2026-07-28/schema.ts:1602 “export interface GetPromptRequestParams extends InputResponseRequestParams {”; 2026-07-28/server/prompts.mdx:165 “Servers MAY also respond to prompts/get with an”; internal/mcpserver/server.go:742-746; internal/mcpserver/ask.go:271-275

MRTR-04 Server-initiated requests removed (ServerRequest union gone)

  • What. 2026 servers cannot send ping, sampling/createMessage, roots/list or elicitation/create as JSON-RPC requests; MRTR is the only way. ClientResult shrinks to EmptyResult.
  • Where. ServerRequest in 2024-11-05..2025-11-25; none in 2026-07-28.
  • mcpx @ 05c78b2. Asks a legacy host on the wire and a 2026 host through input_required; the call does not know which happened (internal/mcpserver/ask.go:117-121).
  • Value to mcpx. + high: done per era.
  • Effort. S.
  • Risk. None.
  • Detail. The changelog calls this a breaking change but never says the ServerRequest union and the non-empty ClientResult members disappeared. ClientResult = EmptyResult is vestigial, since a 2026 client answers nothing.
  • Sources. schema/2025-11-25/schema.ts:2545 “export type ServerRequest =”; schema/2026-07-28/schema.ts:3169 “export type ClientResult = EmptyResult;”; 2026-07-28/basic/patterns/mrtr.mdx:12 “The previous pattern of server-initiated requests is no longer”; internal/mcpserver/ask.go:117-121.

MRTR-05 No input request of a kind the client did not declare

  • What. A server MUST NOT put an elicitation/create (or sampling, or roots) request into inputRequests for a client that did not declare that capability on this request.
  • Where. 2026-07-28; capabilities are per request there.
  • mcpx @ 05c78b2. sendableTo keeps only questions the host can take (internal/mcpserver/ask.go:241-249, via Question.Sendable at internal/mcpserver/conn.go:367). A question the host cannot take (a URL flow to a form-only host, say) stays in the broker for another audience, and mcpx waits (internal/mcpserver/ask.go:177-193).
  • Value to mcpx. + med: correct, and the broker makes the fallback useful.
  • Effort. S.
  • Risk. low.
  • Detail. When the request truly cannot finish without the capability, the spec’s answer is -32021, which mcpx never sends because a broker audience can still answer; see elicitation.md, ERR-08.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:246 “that the client has not declared support for in its capabilities”; internal/mcpserver/ask.go:241-249; internal/mcpserver/ask.go:179 “Either nothing was asked yet”.

MRTR-06 requestState opaque; client echoes it exactly, never invents one

  • What. Clients MUST NOT inspect, parse or modify requestState, MUST send back the exact value, and MUST NOT include one if none was given.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. As a client, echoed as received (internal/mcpclient/modern.go:180-187), with stale requestState and inputResponses removed before each retry (internal/mcpclient/modern.go:171-172).
  • Value to mcpx. + med: done.
  • Effort. S.
  • Risk. None.
  • Detail. The spec lets servers encode anything (“base64-encoded JSON, encrypted JWT, serialized binary”), so a client cannot assume a format; mcpx’s own token happens to be readable base64url JSON (MRTR-07).
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:254 “the client MUST echo back the exact value of that field”; 2026-07-28/basic/patterns/mrtr.mdx:255 “Clients MUST NOT inspect, parse, modify”; internal/mcpclient/modern.go:181 “Opaque: passed back exactly as received”.

MRTR-07 Server MUST integrity-protect requestState

  • What. Servers MUST treat requestState as attacker-controlled; when it influences authorization, resource access or business logic they MUST protect its integrity (HMAC or AEAD) and reject anything that fails verification.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The token is base64url(json{c, b, e}) + "." + base64url(HMAC-SHA256) with a random 32-byte key per process that is never persisted (internal/mcpserver/state.go:47-51, :56-74); verification is constant-time (internal/mcpserver/state.go:82-112). The state comment says the spec “says nothing about what a server should put in it” (internal/mcpserver/state.go:20-21), which is stale against mrtr.mdx.
  • Value to mcpx. + high: a bare call id would let anyone resume, and read, another host’s call.
  • Effort. S — done; fix the comment.
  • Risk. low.
  • Detail. Observed on the wire (M2, decode of the base64url part): {"c":"tsk-685a4a42635a4122", "b":"sess-e4af48671de84810a1c018d965371751","e":1790768865}, that is a daemon task id, the session id, and a Unix expiry. A daemon restart invalidates every token, as intended, because the calls they name do not survive either.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:232 “servers MUST treat requestState as an attacker-controlled input.”; internal/mcpserver/state.go:73 “return body + "." + s.sign(body), nil”; internal/mcpserver/state.go:21 “nothing about what a server should put in it”; wire M2.

MRTR-08 requestState SHOULD bind the authenticated principal

  • What. To prevent replay, the integrity-protected payload SHOULD include the authenticated principal, and state presented by a different principal SHOULD be rejected.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Binds to the connection instead: the binding is the connection id (internal/mcpserver/conn.go:67), and minting refuses an empty one (internal/mcpserver/state.go:64-66). Over HTTP a connection has an id only when the client echoes an Mcp-Session-Id minted on initialize or server/discover; any other request gets an anonymous connection with an empty id (internal/mcpserver/server.go:1209-1218). 2026 removed that header. Wire M4: replaying a valid state without the session header gets -32602 "this requestState belongs to another session".
  • Value to mcpx. + high: this is why a correct stateless 2026 host cannot be asked anything (the resulting -32603 is elicitation.md ELI-17).
  • Effort. M — mcpx has no authenticated principal on /mcp (an auth-register item), so the honest binding today is nothing beyond the HMAC plus the method and params digest (MRTR-10); bind to a principal once one exists.
  • Risk. high if not done: every stateless 2026 host is locked out of MRTR. If done carelessly (no binding, no digest), a leaked token resumes someone else’s call.
  • Detail. The spec binds to the principal, not the connection, and 2026 statelessness says servers SHOULD NOT depend on connection reuse. mcpx’s doc chose the session deliberately (“A token bound to nothing is a token anyone may present”), which was reasonable before the HMAC-plus-digest option was considered.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:235 “the authenticated principal, rejecting state presented by a different principal.”; internal/mcpserver/state.go:103 “if p.Binding != binding {”; internal/mcpserver/conn.go:67; internal/mcpserver/server.go:1209-1218; 2026-07-28/changelog.mdx:12 “Remove protocol-level sessions”; wire M4.

MRTR-09 requestState SHOULD carry a short expiry

  • What. The payload SHOULD include a short TTL, and expired state SHOULD be rejected.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The e field is the Unix expiry, proto.stateTTL (30 m) after minting, and verification refuses it afterwards with “this requestState expired; the call it named is gone” (internal/mcpserver/state.go:109).
  • Value to mcpx. + low: done.
  • Effort. S.
  • Risk. None.
  • Detail. The setting lives in internal/defaults/defaults.json under proto, so it is configurable rather than inline.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:236 “a short expiry (TTL), rejecting state presented after it lapses;”; internal/mcpserver/state.go:109 “if time.Now().Unix() > p.Expires {”; internal/defaults/defaults.json:73 “"stateTTL": "30m",”.

MRTR-10 requestState SHOULD identify method and parameters digest

  • What. The payload SHOULD name the originating request (method and a digest of its salient parameters), and state presented on a different request SHOULD be rejected.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The payload has no method and no digest (internal/mcpserver/state.go:47-51); a retry is resumed by call id alone and its own params are ignored (internal/mcpserver/ask.go:131-141).
  • Value to mcpx. + med: closes cross-request replay, and is what would let mcpx drop the session binding (MRTR-08).
  • Effort. S — hash method plus name plus arguments into the payload and compare on resume.
  • Risk. med: within one session, a state replayed on a different tools/call returns the first call’s result.
  • Detail. forAsk already strips _meta, inputResponses and requestState from params (internal/mcpserver/ask.go:95-108), so the digest input is readily available.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:237 “an identifier for the originating request, e.g. the method name and a digest of its salient parameters”; internal/mcpserver/state.go:47-51; internal/mcpserver/ask.go:131-141.

MRTR-11 Single use needs server-side enforcement

  • What. Principal, TTL and request binding bound the replay window but do not make a state single-use; a server for which a state must be consumed at most once MUST enforce that itself.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Not enforced: verify checks signature, binding and expiry only (internal/mcpserver/state.go:82-112). Replaying a state within its TTL re-polls the same daemon call.
  • Value to mcpx. − moot: an mcpx call is not a one-time redemption, so the MUST does not bind it.
  • Effort. S.
  • Risk. low.
  • Detail. Replaying with fresh answers would feed them to an already-answered question; what the broker does then was not examined.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:240 “do not by themselves guarantee single-use.”; 2026-07-28/basic/patterns/mrtr.mdx:242 “enforce that invariant server-side.”; internal/mcpserver/state.go:82-112.

MRTR-12 Retry carries inputResponses keyed like inputRequests

  • What. InputResponseRequestParams{inputResponses?, requestState?}: each key matches a request key, each value is an ElicitResult, CreateMessageResult or ListRootsResult. Servers SHOULD ignore unexpected keys.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. As a client it answers every key and builds the map (internal/mcpclient/modern.go:163-179). As a server it reads the map as raw JSON and hands it to the broker (internal/mcpserver/ask.go:131-141).
  • Value to mcpx. + med: done.
  • Effort. S.
  • Risk. None known; how the broker treats an unknown key was not examined.
  • Detail. If required answers are missing, the server SHOULD ask again rather than fail (MRTR-15).
  • Sources. schema/2026-07-28/schema.ts:600 “export interface InputResponseRequestParams extends RequestParams {”; schema/2026-07-28/schema.ts:542 “CreateMessageResult | ListRootsResult | ElicitResult;”; 2026-07-28/basic/patterns/mrtr.mdx:264 “the server SHOULD ignore any information it does not recognize or need.”; internal/mcpclient/modern.go:163-179.

MRTR-13 Retry uses a new JSON-RPC id; state only for that retry

  • What. The initial request and the retry are independent, so the id MUST differ; inputRequests and requestState MUST NOT be used for any other request the client sends in parallel.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Every round goes through roundTrip, which takes a fresh id (internal/mcpclient/client.go:681-682).
  • Value to mcpx. + low: done.
  • Effort. S.
  • Risk. None.
  • Detail. Unspecified: whether a retry may reuse the original request’s progressToken. Neither mrtr.mdx nor the progress page says (see progress-cancellation.md).
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:256 “The JSON-RPC id MUST be different”; 2026-07-28/basic/patterns/mrtr.mdx:257 “They MUST NOT be used for any other request”; internal/mcpclient/client.go:681-682.

MRTR-14 requestState-only result: client MAY retry at once

  • What. An InputRequiredResult with no inputRequests asks the client only to come back; it MAY retry immediately. The schema’s example calls this load shedding; the prose never does.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Handled: empty responses, state echoed (internal/mcpclient/modern.go:173-187). The retry is immediate, with no backoff (internal/mcpclient/client.go:654-677).
  • Value to mcpx. + low: a server-directed continuation that mcpx could also use itself.
  • Effort. S — add a backoff.
  • Risk. med: a hot loop against a server that is shedding load, until the round cap (MRTR-16).
  • Detail. “MAY retry immediately” permits waiting; a load-shedding server presumably wants the client to.
  • Sources. schema/2026-07-28/schema.ts:579 “@example InputRequiredResult with request state only (load shedding)”; 2026-07-28/basic/patterns/mrtr.mdx:253 “the client MAY retry the original request immediately.”; internal/mcpclient/client.go:654-677.

MRTR-15 Rounds are unbounded on the server side

  • What. A server MAY answer input_required repeatedly, SHOULD ask again when required information is missing rather than fail, and MUST NOT assume the client will fulfil or retry. No maximum is specified.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. As a server mcpx counts rounds per request and abandons the upstream call past proto.askRounds (8) with -32603 (internal/mcpserver/ask.go:195-199). The cap is also what turns a failed mint into an immediate error (ELI-17 in elicitation.md).
  • Value to mcpx. + low: a guard the spec omits, and configurable.
  • Effort. S.
  • Risk. low: an upstream wizard asking more than eight questions fails through mcpx.
  • Detail. mcpx also bounds the whole hold by proto.askTimeout (10 m). Abandoning after a cap is allowed; the spec only asks servers not to error for missing answers.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:247 “Servers MAY choose to return an InputRequiredResult on multiple attempts”; 2026-07-28/basic/patterns/mrtr.mdx:267 “rather than returning an error.”; internal/mcpserver/ask.go:195-199; internal/defaults/defaults.json:72 “"askRounds": 8,”.

MRTR-16 Client-side cap on rounds

  • What. Nothing caps rounds, so a client needs its own limit.
  • Where. mcpx: elicit.inputRounds (8). opencode v2: its SDK auto-fulfils up to 10 rounds by default. The spec sets none.
  • mcpx @ 05c78b2. Errors after defaults.InputRounds rounds (internal/mcpclient/client.go:671-673; internal/defaults/defaults.go:356).
  • Value to mcpx. + low: sensible.
  • Effort. S — done.
  • Risk. low: a legitimate multi-step server longer than the cap fails.
  • Detail. mcpx has two separate caps of the same value: one as a client of upstreams (this row), one as a server to hosts (MRTR-15).
  • Sources. internal/mcpclient/client.go:671-673; internal/defaults/defaults.json:66 “"inputRounds": 8”; v2:packages/core/src/mcp/client.ts:157; https://unpkg.com/@modelcontextprotocol/client@2.0.0/dist/src-D_zzAWoS.mjs (L5049, DEFAULT_INPUT_REQUIRED_MAX_ROUNDS = 10).

MRTR-17 No per-input error channel

  • What. InputResponse is a union of results only. A client that cannot or will not fulfil one input request (malformed sampling, an unsupported mode) cannot say so for that key: it can omit it, answer decline/cancel (elicitation only), or give up. Yet the 2026 schema still documents client-side -32602 cases for sampling and elicitation that MRTR has no way to carry.
  • Where. 2026-07-28. Legacy clients returned JSON-RPC errors to the server’s request.
  • mcpx @ 05c78b2. As a client, any failed answer aborts the whole call with a local error, naming the key (internal/mcpclient/modern.go:164-168), rather than answering the others.
  • Value to mcpx. + low: answering what it can and omitting the rest would let the server re-ask or fail cleanly.
  • Effort. M.
  • Risk. low.
  • Detail. Omitting a key is the only in-band signal, and the server SHOULD then ask again (MRTR-15), which can loop until a cap.
  • Sources. schema/2026-07-28/schema.ts:541 “export type InputResponse =”; schema/2026-07-28/schema.ts:371 “Sampling: Missing tool result or tool results mixed with other content”; internal/mcpclient/modern.go:164-168.

MRTR-18 Client fulfils input_required and retries

  • What. The client side of MRTR: answer each input request with the matching handler and retry the original request.
  • Where. mcpx (client of modern upstreams). opencode v2: the SDK fulfils automatically from registered handlers, which are only elicitation/create and roots/list. opencode v1 speaks no 2026. lootbox uses SDK 1.22, whose newest revision is 2025-06-18.
  • mcpx @ 05c78b2. Modern results with resultType: "input_required" are answered by answer(), the same function that serves legacy server-initiated requests, so questions go through the same broker; the call is retried with inputResponses and the echoed state (internal/mcpclient/client.go:654-677; internal/mcpclient/modern.go:103-135, :156-189).
  • Value to mcpx. + high: parity across eras for upstream questions.
  • Effort. S — done.
  • Risk. med: unreachable in practice until mcpx’s modern discovery and headers work against conformant servers (a lifecycle and transports issue), so this path is untested against real 2026 upstreams.
  • Detail. opencode v2 fails an input request it has no handler for (sampling) after the SDK refuses, which is the missing error channel of MRTR-17 seen from the client.
  • Sources. internal/mcpclient/client.go:654-677; internal/mcpclient/modern.go:103-135; v2:packages/core/src/mcp/client.ts:155; v2:packages/core/src/mcp/client.ts:157; .lootbox/deno.json:44 “@modelcontextprotocol/sdk”; https://unpkg.com/@modelcontextprotocol/client@2.0.0/dist/src-D_zzAWoS.mjs (L5044, DEFAULT_INPUT_REQUIRED_AUTO_FULFILL = true).

MRTR-19 Results returned through MRTR keep their full shape

  • What. After the rounds, the final result is the method’s own result: a full CallToolResult, GetPromptResult or ReadResourceResult.
  • Where. 2026-07-28 (and the same holds for legacy interruptible calls).
  • mcpx @ 05c78b2. askResult renders a finished interruptible call as one text block: tools/call gets one text content (plus isError), prompts/get one user text message, resources/read one text entry (internal/mcpserver/ask.go:269-296). Images, structuredContent, resource_link, multi-message prompts and blobs are lost on this path, though the direct path keeps more.
  • Value to mcpx. + med: a call should not return less because it happened to ask a question.
  • Effort. M — carry the raw upstream result through the daemon task instead of Outcome.Text.
  • Risk. med: MRTR-routed calls silently degrade compared with the same call made without a question.
  • Detail. The same renderer serves legacy hosts whose calls were interrupted on the wire, so the loss is not 2026-only.
  • Sources. internal/mcpserver/ask.go:269-296; internal/mcpserver/ask.go:269 “func askResult(req request, out Outcome) map[string]any {”.

MRTR-20 Where the interrupted call’s state lives

  • What. MRTR lets a server ask “without maintaining any server-side state”: everything needed to resume can travel in requestState. mcpx instead keeps the upstream call running and puts only its handle in the token.
  • Where. 2026-07-28 describes the stateless option; it is not a MUST.
  • mcpx @ 05c78b2. An interruptible call is a daemon task tracked in an askTable (internal/daemon/routes_proto.go:53-65); the token’s c is its task id (wire M2). If the host disconnects, the call “keeps running as a task” (internal/mcpserver/ask.go:157-162), and its result is kept for proto.askTTL (15 m).
  • Value to mcpx. + med: a live upstream call cannot be serialised into a token, so this is the server-minted-handle pattern, and it is right for a proxy. What it costs is that a resume must reach the same daemon.
  • Effort. L to change, and not worth changing.
  • Risk. med: a disconnect does not cancel the upstream call on this path (unlike ordinary calls; see progress-cancellation.md), and these tasks appear in mcpx_tasks_list but not in MCP tasks/list, because the two task stores differ (tasks.md).
  • Detail. Resuming therefore needs both a valid token and the task still alive: after a daemon restart the key and the call are both gone, which the state comment calls intended.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:199 “without maintaining any server-side state”; internal/mcpserver/ask.go:159 “keeps running as a task, and its questions keep their”; internal/daemon/routes_proto.go:53-65; internal/defaults/defaults.json:75 “"askTTL": "15m",”; internal/mcpserver/state.go:27-28 “The key is per process and never persisted.”; wire M2.

Subscriptions and notifications

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  subscriptions/listen and the listen-stream rules of 2026, list_changed delivery per revision, request-scoped notifications, and notification hygiene in mcpx and opencode

Until 2025-11-25 a server pushes change notifications (…/list_changed, resources/updated after resources/subscribe) whenever it has a channel to the client. 2026-07-28 replaces that with one long-lived request, subscriptions/listen, whose filter says what the client wants; every message on it is tagged with the listen request’s id, and progress and log messages stay on the stream of the request they belong to. mcpx implements the listen request on stdio but gets the acknowledgement and the tagging wrong, answers it over HTTP with an empty 202, and, as a client, never subscribes to anything upstream. Other notifications are registered with their feature: notifications/cancelled and notifications/progress in progress-cancellation.md, notifications/message in logging.md, notifications/roots/list_changed in roots.md, notifications/elicitation/complete in elicitation.md, notifications/tasks/status and notifications/tasks in tasks.md; notifications/initialized goes with lifecycle, and resources/subscribe, resources/updated and mcpx’s mcpx:// URI rewriting (its R-7 and R-8) go with resources.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
SUB-01subscriptions/listen, a long-lived notification request2026-07-28 has26-07 onlypartial — works on stdio; HTTP see SUB-12+ highMmed
SUB-02SubscriptionFilter: three list flags and resourceSubscriptions[]2026-07-28 has26-07 only (+ taskIds from the tasks extension)✓ — four fields mapped to daemon event kinds+ medSlow
SUB-03Acknowledgement carries notifications and _meta subscription id2026-07-28 has mcpx missing26-07 only✗ — params.subscriptionId; no notifications field (#208)+ medSmed
SUB-04Acknowledgement first; nothing on the subscription before it2026-07-28 has mcpx missing26-07 only✗ — listener starts before the ack is written (#208)+ lowSlow
SUB-05Every listen-stream message tagged with _meta subscription id2026-07-28 has specs conflict mcpx missing26-07 only✗ — list_changed and updated sent untagged (#208)+ medSmed
SUB-06list_changed delivered only on an opted-in listen stream2024-11-05 has 2026-07-28 has24-11..25-11 unsolicited · 26-07 listen only✓ — gated by the filter+ medSlow
SUB-07Progress and log messages stay on their request’s stream2026-07-28 has25-03..25-11 SHOULD relate · 26-07 MUST relate, never on listenn/a — mcpx sends neither to hosts+ medMmed
SUB-08Several concurrent subscriptions per client2026-07-28 has mcpx missing26-07 only✗ — a second listen replaces the first (#208)+ medMmed
SUB-09Client ends a subscription: close SSE, or notifications/cancelled on stdio2026-07-28 has mcpx missing26-07 only✗ — stdio cancel is recorded; the stream continues (#208)+ medSmed
SUB-10Graceful end: SubscriptionsListenResult with required subscription id2026-07-28 has mcpx missing26-07 only✗ — never sent; stdio exit ends silently (#208)+ lowSlow
SUB-11Server teardown: notifications/cancelled versus a final result2026-07-28 has specs conflict mcpx missing26-07 only✗ — neither sent; comment says the transport sends one (#208)+ lowSlow
SUB-12subscriptions/listen over Streamable HTTP2026-07-28 has mcpx missing 2025-03-26 has26-07 only✗ — 202 Accepted, empty body (wire W17) (#208)+ highMhigh
SUB-13subscriptions/listen from a legacy client2026-07-28 has26-07 methodacc. — accepted on stdio from any era+ lowSlow
SUB-14stdio reconnect: client re-sends listen; server keeps no state2026-07-28 has26-07 only✓ — subscription state dies with the stdio process+ lowSlow
SUB-15Client subscribes upstream for changes2026-07-28 has opencode v2 has mcpx missing 2024-11-05 hasspec: listen (26-07) or resources/subscribe; opencode v2 SDK listens✗ — never listens, never subscribes upstream (#200)+ medMmed
SUB-16Client reaction to list_changedopencode v1 has opencode v2 hasopencode v1 tools only; v2 tools, prompts, resources✓ — invalidates the upstream schema cache+ lowSlow
SUB-17Client notifications shrink to notifications/cancelled only2026-07-28 removes24-11..25-11 four or five kinds · 26-07 one✓ — sends no client progress or roots notifications− mootSlow
SUB-18A notification never gets a responsemcpx missingevery revision✗ — unknown notifications answered with -32601 (wire S5) (#202)+ medSmed

SUB-01 subscriptions/listen, a long-lived notification request

  • What. A client request that opens a notification stream from the server, with a notifications filter. It replaces the legacy HTTP GET stream and resources/subscribe, and means the same on HTTP and stdio. Its response arrives only when the stream ends gracefully (SUB-10).
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. Implemented and accepted from any era (internal/mcpserver/server.go:622-640); the daemon notifier maps the filter to its event bus (internal/cli/serve.go:726-763). Without a notifier it answers -32601 “this mcpx pushes no notifications”. On stdio it works apart from SUB-03, SUB-05 and SUB-08..SUB-11; over HTTP it does not (SUB-12).
  • Value to mcpx. + high: the only way a 2026 host learns that mcpx’s resources or prompts changed.
  • Effort. M — mostly the HTTP half.
  • Risk. med: see the rows below.
  • Detail. The daemon notifier sends only kinds the filter asked for, as the spec requires (“a server MUST NOT send what the client did not ask for”, in the code’s words).
  • Sources. schema/2026-07-28/schema.ts:1315 “subscriptions/listen”; 2026-07-28/basic/patterns/subscriptions.mdx:7 “opens a long-lived notification stream from the server to the”; internal/mcpserver/server.go:622-640; internal/cli/serve.go:726-763.

SUB-02 SubscriptionFilter: three list flags and resourceSubscriptions[]

  • What. toolsListChanged, promptsListChanged, resourcesListChanged (booleans) and resourceSubscriptions (URIs). All optional; omitting one means not subscribed; the server MUST NOT send types that were not requested.
  • Where. 2026-07-28 only. The tasks extension adds taskIds (tasks.md).
  • mcpx @ 05c78b2. All four map to daemon event kinds (internal/cli/serve.go:726-763; struct at internal/mcpserver/server.go:77-80). resourceSubscriptions URIs are stripped of mcpx://<ns>/ to match upstream events; that loses the namespace, which the resources register covers.
  • Value to mcpx. + med: done.
  • Effort. S.
  • Risk. low.
  • Detail. taskIds is not accepted, consistent with mcpx having no extension tasks.
  • Sources. schema/2026-07-28/schema.ts:1270 “export interface SubscriptionFilter {”; 2026-07-28/basic/patterns/subscriptions.mdx:49 “All fields are optional. Omitting a field is equivalent to not subscribing to that”; internal/cli/serve.go:726-763.

SUB-03 Acknowledgement carries notifications and _meta subscription id

  • What. For each subscription the server MUST first send notifications/subscriptions/acknowledged with params.notifications set to the subset of the filter it will honour, and the listen request’s id in _meta["io.modelcontextprotocol/subscriptionId"].
  • Where. 2026-07-28 only. The changelog says only that “the server acknowledges”; the notification is not named there.
  • mcpx @ 05c78b2. Sends params: {"subscriptionId": <id>}: the id as a top-level param instead of the _meta key, and no notifications field (internal/mcpserver/server.go:637-639). Wire S4: {"method":"notifications/subscriptions/acknowledged","params":{"subscriptionId":4}}.
  • Value to mcpx. + med: clients use the echoed filter to learn which kinds are unsupported, and the id to correlate.
  • Effort. S.
  • Risk. med: a schema-invalid notification; a strict client may never believe the stream is live.
  • Detail. Unsupported kinds are simply omitted from the echoed filter. The 2026 _meta key registration is in the _meta register.
  • Sources. schema/2026-07-28/schema.ts:1399 “notifications/subscriptions/acknowledged”; schema/2026-07-28/schema.ts:1378 “notifications: SubscriptionFilter;”; 2026-07-28/basic/patterns/subscriptions.mdx:61 “reflects the subset the server agreed to”; internal/mcpserver/server.go:638 “"subscriptionId": json.RawMessage(req.ID),”; wire S4.

SUB-04 Acknowledgement first; nothing on the subscription before it

  • What. No notification for a subscription may precede its acknowledgement. On stdio the ordering is per subscription id: messages of other subscriptions MAY come first.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. restartListen starts the listener goroutine (internal/mcpserver/server.go:1427-1444) before the acknowledgement is pushed (internal/mcpserver/server.go:636-639), so a change arriving in that window can be written first.
  • Value to mcpx. + low: a narrow race.
  • Effort. S — push the ack, then start listening.
  • Risk. low.
  • Detail. Found as a code-order nuance, not observed on the wire.
  • Sources. 2026-07-28/basic/patterns/subscriptions.mdx:56 “send any notification on the”; 2026-07-28/basic/patterns/subscriptions.mdx:57; internal/mcpserver/server.go:636-639.

SUB-05 Every listen-stream message tagged with _meta subscription id

  • What. Every notification on a listen stream (and the acknowledgement and the graceful-close result) MUST carry _meta["io.modelcontextprotocol/subscriptionId"] = the listen request’s id; on stdio clients MUST use it to demultiplex.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. list_changed and resources/updated go out with empty or {uri} params and no _meta (internal/events/events.go:252-261); the stdio push adds nothing (internal/mcpserver/server.go:1010-1012).
  • Value to mcpx. + med: required for stdio hosts with more than one subscription.
  • Effort. S — tag in the push closure of a listen, not in the event mapper.
  • Risk. med: 2026 hosts cannot attribute notifications.
  • Detail. The same notification builder serves legacy resources/subscribe listeners, which must not get the key. The 2026 prose examples of notifications/tools/list_changed and …/prompts/list_changed omit the tag themselves, contradicting the MUST.
  • Also recorded from the meta register. The acknowledgement sends {"subscriptionId": <id>} as a top-level param with no _meta (internal/mcpserver/server.go:637-639; wire S4). list_changed and resources/updated are built without _meta (internal/events/events.go:252-271) and pushed as is (internal/mcpserver/conn.go:78-85). The client never opens a listen stream, so the client side is not exercised. The same notification builders serve legacy resources/subscribe clients, for whom the key must not be added. 2026’s own prose example of notifications/tools/list_changed omits the key. The acknowledgement’s missing notifications field and the rest of listen are in the notifications register.
  • Sources. schema/2026-07-28/schema.ts:133 “"io.modelcontextprotocol/subscriptionId"?: RequestId;”; 2026-07-28/basic/index.mdx:412 “On notifications delivered via a”; 2026-07-28/basic/index.mdx:413; 2026-07-28/server/tools.mdx:246 “"method": "notifications/tools/list_changed"”; 2026-07-28/server/prompts.mdx:177; internal/events/events.go:252-261.; schema/2026-07-28/schema.ts:1334; internal/mcpserver/server.go:638 “"subscriptionId": json.RawMessage(req.ID),”; internal/events/events.go:255; wire S4.

SUB-06 list_changed delivered only on an opted-in listen stream

  • What. notifications/{tools,prompts,resources}/list_changed exist in every revision. Until 2025-11-25 a server that declared listChanged may send them unsolicited; in 2026 listChanged: true means the server sends them only on a listen stream whose filter asked.
  • Where. All five; the delivery model changes in 2026-07-28.
  • mcpx @ 05c78b2. For 2026 (listen) delivery is gated by the filter (internal/cli/serve.go:728-736). The legacy half is CAP-20.
  • Value to mcpx. + med: done for 2026 on stdio.
  • Effort. S.
  • Risk. low: a 2026 host that never opens a listen never hears of changes, by design.
  • Detail. The capability flags are unchanged in shape; only what they promise changed. The capabilities register owns the flags themselves.
  • Sources. schema/2024-11-05/schema.ts:681 “notifications/tools/list_changed”; schema/2025-11-25/schema.ts:1159 “This may be issued by servers without any previous subscription from the client.”; schema/2026-07-28/schema.ts:1888 “This is only delivered on a”; 2026-07-28/server/tools.mdx:238 “When the list of available tools changes, servers that declared the”; internal/cli/serve.go:728-736.

SUB-07 Progress and log messages stay on their request’s stream

  • What. 2026: notifications/progress and notifications/message flow only on the response stream of the request they relate to (they MUST relate to it), before its response, and never on a listen stream, which carries only opted-in change notifications. Legacy servers could send notifications on a POST’s stream that SHOULD relate to the request, or on the standalone GET stream.
  • Where. SHOULD relate in 2025-03-26..2025-11-25; MUST relate, and listen-free, in 2026-07-28.
  • mcpx @ 05c78b2. Sends neither progress nor log messages to hosts (progress-cancellation.md, logging.md).
  • Value to mcpx. + med: the rule to follow when mcpx starts relaying upstream progress: route it to the host request that caused it, never to a shared bus or a listen stream.
  • Effort. M — per-call correlation; on stdio all streams share one channel, so correlation is by progressToken or request context.
  • Risk. med: cross-host leakage of progress or logs in a pooled proxy if done wrong.
  • Detail. The same rule is stated three times in 2026: the changelog, the transport page, and the logging page. The tasks extension further forbids both on a task’s listen stream (tasks.md).
  • Sources. 2026-07-28/changelog.mdx:18 “Request-scoped notifications such as”; 2026-07-28/basic/transports/streamable-http.mdx:115 “before the final response. These notifications MUST relate to the”; 2025-03-26/basic/transports.mdx:112 “These messages SHOULD relate to the originating client”; 2026-07-28/server/utilities/logging.mdx:69 “the server MUST NOT deliver it on a”.

SUB-08 Several concurrent subscriptions per client

  • What. A client MAY hold several subscriptions at once (say, tools changes and resource updates), each identified by its own listen request id.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. One stream per connection: a new listen stops the previous listener (internal/mcpserver/server.go:1427-1444). The comment calls replacement “the simplest faithful implementation”, but the spec explicitly allows concurrency.
  • Value to mcpx. + med: match the spec.
  • Effort. M — per-subscription listeners, which also needs SUB-05’s tagging to be useful.
  • Risk. med: a host that opens two subscriptions silently loses the first.
  • Detail. The previous listen request never gets its graceful-close result either (SUB-10).
  • Sources. 2026-07-28/basic/patterns/subscriptions.mdx:109 “A client MAY have multiple active subscriptions concurrently”; internal/mcpserver/server.go:1425-1426 “the simplest faithful implementation”; internal/mcpserver/server.go:1427-1444.

SUB-09 Client ends a subscription: close SSE, or notifications/cancelled on stdio

  • What. On HTTP the client closes the listen’s SSE stream; on stdio it sends notifications/cancelled naming the listen request id.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. On stdio the cancel is only recorded (internal/mcpserver/server.go:1521-1538); the listener keeps running and the host keeps receiving. Over HTTP there is no listen stream to close (SUB-12).
  • Value to mcpx. + med: a host that unsubscribes should stop receiving.
  • Effort. S — when the cancelled id is the active listen, stop it.
  • Risk. med: an unsubscribed host keeps getting notifications.
  • Detail. The general inbound-cancel gap is in progress-cancellation.md.
  • Sources. 2026-07-28/basic/patterns/subscriptions.mdx:120-121; internal/mcpserver/server.go:1533 “c.cancelled[fmt.Sprint(p.RequestID)] = p.Reason”.

SUB-10 Graceful end: SubscriptionsListenResult with required subscription id

  • What. When the server ends a subscription itself (shutdown, say), it SHOULD answer the original listen request with a complete result whose _meta carries the subscription id (required on this result type). A transport that closes without it is an unexpected disconnect, which the client MAY treat as a reason to reconnect.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. Never sends it: the listen request gets no reply while open (internal/mcpserver/server.go:622-640), and ServeStdio stops the listener on exit without a message (internal/mcpserver/server.go:1014).
  • Value to mcpx. + low: lets clients tell shutdown from failure.
  • Effort. S.
  • Risk. low: clients may reconnect-loop on a clean shutdown.
  • Detail. SubscriptionsListenResultMetaObject makes the key required here, unlike on ordinary results.
  • Sources. schema/2026-07-28/schema.ts:1334 “"io.modelcontextprotocol/subscriptionId": RequestId;”; 2026-07-28/basic/patterns/subscriptions.mdx:132 “with a completion result before closing the stream.”; 2026-07-28/basic/patterns/subscriptions.mdx:158 “treat as a trigger to reconnect.”; internal/mcpserver/server.go:1014.

SUB-11 Server teardown: notifications/cancelled versus a final result

  • What. Three 2026 texts disagree on how a server ends a subscription. The cancellation page: the server MUST send notifications/cancelled naming the listen id when it tears the subscription down, and for nothing else. The subscriptions page: it SHOULD send a successful listen result, then close. The schema: the server sends that cancel “On stdio” only.
  • Where. 2026-07-28 only. Legacy servers could cancel any request they had issued.
  • mcpx @ 05c78b2. Sends neither. The listen comment says “the transport sends the terminating notifications/cancelled when the client closes it” (internal/mcpserver/server.go:624-626); no code does, and the comment has the direction backwards (a client closing is a client cancel).
  • Value to mcpx. + low: on a graceful stdio teardown, send both the cancel and the result, which satisfies all three texts.
  • Effort. S.
  • Risk. low: clients waiting for one signal see the other, or nothing.
  • Detail. On HTTP it is unclear whether a notifications/cancelled should appear on a listen SSE stream that otherwise carries only opted-in kinds. Server-sent cancellations MUST reference a subscriptions/listen id.
  • Sources. 2026-07-28/basic/patterns/cancellation.mdx:11 “A server MUST send”; 2026-07-28/basic/patterns/subscriptions.mdx:122 “it SHOULD send a”; schema/2026-07-28/schema.ts:637 “On stdio, the server also sends this notification, solely to terminate a”; 2026-07-28/basic/patterns/cancellation.mdx:71 “Server-sent cancellation notifications MUST reference a”; internal/mcpserver/server.go:624-626.

SUB-12 subscriptions/listen over Streamable HTTP

  • What. A JSON-RPC request over HTTP MUST get either application/json or text/event-stream. For a listen that means an SSE stream carrying the acknowledgement and then the notifications.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. An HTTP connection has no push function, so restartListen returns at once (internal/mcpserver/server.go:1433-1437), the acknowledgement goes nowhere, handle returns nil (internal/mcpserver/server.go:640), and ServeHTTP writes 202 Accepted with no body (internal/mcpserver/server.go:1188-1190). Wire W17. docs/protocol.md:84 presents listen as working in any era.
  • Value to mcpx. + high: remote 2026 hosts can never hear of changes otherwise.
  • Effort. M to stream (hold the POST open and push SSE; mcpx already streams SSE for legacy elicitation); S to refuse with an error.
  • Risk. high: a 2026 client waits forever for an acknowledgement, or treats 202 as a transport error.
  • Detail. The capabilities are truthfully listChanged: false over HTTP (wire W5), yet the method is “accepted”.
  • Also recorded from the transports register. Any request whose handler returns no response gets 202 (internal/mcpserver/server.go:1188-1190). That happens to subscriptions/listen over HTTP: the HTTP connection has no push function, the listener returns early (internal/mcpserver/server.go:1433-1437), the acknowledgement goes nowhere and the handler returns nil (internal/mcpserver/server.go:640). Wire W17: 202 Accepted, empty body. Listen semantics are in the notifications register; this row is the transport rule.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:88-90 “If the body is a JSON-RPC request, the server MUST return either”; internal/mcpserver/server.go:1188-1190; docs/protocol.md:84; wire W17.; 2026-07-28/basic/transports/streamable-http.mdx:88 “6. If the body is a JSON-RPC request, the server MUST return either”

SUB-13 subscriptions/listen from a legacy client

  • What. A 2025-06-18 stdio client can open a 2026-style listen and get notifications.
  • Where. The method is 2026-07-28’s.
  • mcpx @ 05c78b2. Accepted on purpose (internal/mcpserver/server.go:622; docs/protocol.md:84); wire S4 is a legacy client doing exactly this. It is not declared to legacy clients.
  • Value to mcpx. + low: accept-liberally, harmless.
  • Effort. S — done.
  • Risk. None.
  • Detail. The acknowledgement a legacy client gets has the same wrong shape (SUB-03).
  • Sources. internal/mcpserver/server.go:622; docs/protocol.md:84; wire S4.

SUB-14 stdio reconnect: client re-sends listen; server keeps no state

  • What. On stdio, after the connection is re-established the client MUST send subscriptions/listen again; the server holds no subscription state across reconnections.
  • Where. 2026-07-28 only.
  • mcpx @ 05c78b2. As a server, subscription state lives on the stdio connection and dies with the process (internal/mcpserver/server.go:1014). As a client it never listens, so it has nothing to re-send (SUB-15).
  • Value to mcpx. + low: nothing to change server-side.
  • Effort. S.
  • Risk. low.
  • Detail. None.
  • Sources. 2026-07-28/basic/patterns/subscriptions.mdx:161 “client MUST re-send”; 2026-07-28/basic/patterns/subscriptions.mdx:162 “the server holds no subscription state across reconnections.”.

SUB-15 Client subscribes upstream for changes

  • What. To learn about upstream changes a client opens subscriptions/listen to a 2026 server (the only way to get list_changed there) or calls resources/subscribe on a legacy one; unsolicited legacy list_changed needs no request.
  • Where. 2026 listen; legacy resources/subscribe. opencode v2’s SDK opens the listen itself on modern connections.
  • mcpx @ 05c78b2. The pool never opens a listen to a modern upstream and never calls resources/subscribe on a legacy one; Client.SubscribeResource exists and nothing calls it (internal/mcpclient/client.go:536-543). Its comment defers modern servers to “the listening stream”, which the client never opens (internal/mcpclient/client.go:532-535). Cache invalidation therefore depends on unsolicited legacy notifications (internal/daemon/hooks.go:37-51).
  • Value to mcpx. + med: fresh catalogues from modern and remote upstreams.
  • Effort. M.
  • Risk. med: stale tool lists from modern upstreams until mcpx refresh.
  • Detail. A modern listen would also have to be re-opened after an upstream stdio restart (SUB-14’s MUST applies to mcpx as a client).
  • Also recorded from the resources register. Every revision (by one mechanism or the other). Client.SubscribeResource exists (internal/mcpclient/client.go:536-543) and nothing calls it; the pool never opens subscriptions/listen to a modern upstream. A host’s subscription starts a listener on the daemon’s event bus, which sees resources/updated only if an upstream sends it unasked. docs/protocol.md:83 says subscribe “forwards from the same bus”. SubscribeResource is a deliberate no-op for modern upstreams, deferring to “the listening stream”, which does not exist (notifications register).
  • Sources. internal/mcpclient/client.go:532-535 “that is handled by”; internal/mcpclient/client.go:536-543; internal/daemon/hooks.go:37-51; v2:packages/core/src/mcp/client.ts:132 “the SDK opens the subscriptions/listen stream behind these itself.”.; internal/mcpclient/client.go:536 “func (c *Client) SubscribeResource(ctx context.Context, uri string) error {”; docs/protocol.md:83

SUB-16 Client reaction to list_changed

  • What. What a client does when told a list changed.
  • Where. opencode v1 handles tools/list_changed only, and only when the server declares tools: it re-lists tools and publishes ToolsChanged. opencode v2 re-lists tools (reconcile debounced by 100 ms), refreshes prompts (republished as slash commands) and publishes ResourcesChanged.
  • mcpx @ 05c78b2. As a client, list_changed invalidates the upstream schema cache (internal/daemon/hooks.go:37-51; internal/mcpclient/client.go:495-500). As a server, its own tool list is fixed once built, so a host refreshing mcpx’s tools gains nothing; prompts and resources are where changes would show (CAP-20).
  • Value to mcpx. + low: v2 hosts would show mcpx prompt changes live if mcpx sent them.
  • Effort. S.
  • Risk. low.
  • Detail. v1 fetches prompts fresh on every command listing, so ignoring prompts/list_changed costs it little.
  • Sources. v1:packages/opencode/src/mcp/index.ts:462; v1:packages/opencode/src/mcp/index.ts:461; v2:packages/core/src/tool/mcp.ts:140; v2:packages/core/src/mcp/index.ts:374; v2:packages/core/src/mcp/index.ts:375; internal/daemon/hooks.go:37-51.

SUB-17 Client notifications shrink to notifications/cancelled only

  • What. 2026 clients can no longer send notifications/initialized, notifications/progress, notifications/roots/list_changed or notifications/tasks/status; ClientNotification is CancelledNotification alone.
  • Where. Legacy client notifications: cancelled, progress, initialized, roots/list_changed (+ tasks/status in 2025-11-25).
  • mcpx @ 05c78b2. As a client it sends no progress and declares roots without listChanged, so only initialized (legacy) and cancelled go upstream.
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. Client-to-server progress vanishes because server-initiated requests vanish; the changelog never says so. Over 2026 HTTP not even cancelled is defined (see progress-cancellation.md).
  • Sources. schema/2025-11-25/schema.ts:2525 “export type ClientNotification =”; schema/2026-07-28/schema.ts:3166 “export type ClientNotification = CancelledNotification;”.

SUB-18 A notification never gets a response

  • What. In every revision the receiver of a notification MUST NOT reply.
  • Where. Every revision (JSON-RPC and MCP both).
  • mcpx @ 05c78b2. Any notification other than notifications/initialized, initialized and notifications/cancelled falls through to fail(-32601, "no method …") (internal/mcpserver/server.go:766), and the stdio loop writes it (internal/mcpserver/server.go:1063-1066); over HTTP it goes out as 200 with an error body instead of 202. Wire S5: notifications/roots/list_changed and notifications/progress each got an id-less -32601.
  • Value to mcpx. + med: protocol hygiene; legacy hosts that declare roots.listChanged trigger it on every change.
  • Effort. S — return nothing when the message has no id; an unknown request must still get -32601.
  • Risk. med: hosts log or fail on unexpected error frames.
  • Detail. A send-conservatively conflict. Wire W26 shows the known case working: notifications/cancelled over HTTP gets 202.
  • Sources. 2025-03-26/basic/index.mdx:78 “The receiver MUST NOT send a response.”; 2026-07-28/basic/index.mdx:160 “The receiver MUST NOT send a response.”; internal/mcpserver/server.go:766 “return fail(codeMethodNotFound, "no method "+req.Method)”; internal/mcpserver/server.go:1063-1066; wire S5, W26.

Pagination and caching

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-10-29T02:40:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  cursor rules per revision, 2026 cacheable results (ttlMs, cacheScope), list stability, and how mcpx and opencode page and cache

Every list method pages with an opaque cursor in every revision; 2026 adds caching hints (ttlMs, cacheScope) that six results MUST carry, and tightens what a cursor and a list may be. PG-08..PG-13 were trued up on 2026-10-29 (#1604, Phase 3 of design #1601): mcpx’s server now stamps both cache fields on complete cacheable results for modern peers, and its client reads, aggregates and honours them – the pool catalogue cache expires at the earliest declared ttlMs for modern upstreams, and cacheScope: "private" catalogues never reach the disk cache. What matters most in the rest of this register at 05c78b2 is that its client stops paging on an empty nextCursor, which 2026 says is a valid cursor. Deterministic tools/list order is TOOL-23 in the tools register.

Status column: mcpx @ 05c78b2, except PG-08..PG-13, which read mcpx @ c58507a (post-#1601, trued up by #1604). Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
PG-01opaque cursors; “don’t persist across sessions” dropped in 20262024-11-05 has 2026-07-28 has24-11..25-11 ✓ (session-bound) · 26-07 ✓ (no sessions)✓ — stateless encoded offsets+ lowSlow
PG-02"" is a valid cursor and MUST NOT end the list2026-07-28 has mcpx missing26-07 ✓ only · opencode v1 ✓ (only undefined ends)✗ — client stops paging on "" (#200)+ medSmed
PG-03invalid cursor SHOULD be -326022024-11-05 has mcpx missingall five ✓ (SHOULD)✗ — undecodable cursor silently restarts at zero (#202)+ lowSlow
PG-04servers SHOULD provide stable cursors2024-11-05 hasall five ✓ (SHOULD); 26-07 no cross-page guaranteepartial — offsets shift if the list changes+ lowSlow
PG-05page size is the server’s choice2024-11-05 hasall five ✓✓ — mcp.pageSize, default 100+ lowSlow
PG-06client page-following capsopencode v1 has opencode v2 hasopencode v1 1000 pages · v2 64 pages · spec silentpartial — 100 pages, silent truncation, no loop check+ lowSlow
PG-07mcpx client swallows list errors and skips template pages2024-11-05 has mcpx missingmcpx client✗ — prompts/list and templates errors hidden (#203)+ lowSmed
PG-08six results MUST carry cache hints; changelog names five2026-07-28 has specs conflict26-07 ✓ only✓ — server stamps both fields on complete cacheable results for modern peers (#199); client reads and aggregates them on list results (#1601 Phase 1, PR #1608)+ medSmed
PG-09ttlMs: integer ms ≥ 0; absent means 02026-07-28 has26-07 ✓ only✓ — emitted by the server (Cache.List / Cache.Read); honoured by the client: a modern pool cache expires at fetch + min declared ttlMs, absent/0/negative is immediately stale (#1602)+ medSmed
PG-10cacheScope: "public" or "private"2026-07-28 has26-07 ✓ only✓ — emitted by the server; honoured by the client at the disk boundary: private catalogues never persist, public entries carry deadline + scope (#1603, PR #1705)+ medShigh
PG-11per-page caching: own TTL clock, same scope on every page2026-07-28 has26-07 ✓ only✓ by construction — pages are aggregated into one catalogue cache with the minimum TTL across pages and private if any page is private; an expired catalogue is re-fetched whole, never per page (#1601)+ lowSlow
PG-12MRTR retries and input_required results never cached2026-07-28 has26-07 ✓ only✓ — mcpx caches no MRTR result in either direction; the server stamps hints only on resultType: "complete" results, so input_required carries none+ lowSlow
PG-13mcpx’s upstream schema cache ignores ttlMs2026-07-28 has26-07 (client MAY cache per ttlMs)✓ (modern era) — the pool cache expires at the declared deadline and a failed refetch of an expired entry serves stale for that call only; legacy upstreams keep cache-until-list_changed by design (#1601); disk persistence is scope-aware (#1603)+ medSmed
PG-14lists MUST NOT vary per connection; MAY vary by authorization2026-07-28 has26-07 ✓ only (tools, prompts, resources)✓ — lists are per daemon+ medSmed

PG-01 opaque cursors and persistence

  • What. Cursors are opaque strings in every revision. 2024-11-05 through 2025-11-25 also tell clients not to persist cursors across sessions; 2026 drops that line (there are no sessions) and replaces it with PG-02’s rule.
  • Where. All five; the persistence rule is gone in 2026.
  • mcpx @ 05c78b2. nextCursor is base64url("o:<offset>") (internal/mcpserver/server.go:1477-1505), encoded so that clients are not tempted to parse it (internal/mcpserver/server.go:1472-1476). It depends on no server state, so it stays valid across connections — what 2026 now permits.
  • Value to mcpx. + already stateless.
  • Effort. S.
  • Risk. Low.
  • Detail. Being stateless, the cursor is also unsigned and unversioned (PG-04).
  • Sources. 2024-11-05/server/utilities/pagination.mdx:17 “- The cursor is an opaque string token, representing a position in the result set”; 2025-11-25/server/utilities/pagination.mdx:93 “- Don’t persist cursors across sessions”; internal/mcpserver/server.go:1477-1505

PG-02 the empty-string cursor

  • What. 2026: clients MUST NOT decide anything from a cursor’s value except whether a non-null one was given; an empty string is a valid cursor and MUST NOT be treated as the end of results. Only an absent or null nextCursor ends the list.
  • Where. 2026 only. opencode v1 already stops only on undefined.
  • mcpx @ 05c78b2. As a server it never emits "" (it adds nextCursor only when non-empty, internal/mcpserver/server.go:551-555). As a client it stops when NextCursor == "" (internal/mcpclient/client.go:744, internal/mcpclient/client.go:766, internal/mcpclient/client.go:820), and a Go string cannot tell "" from absent, so a 2026 upstream that uses "" as a real cursor is read as one page.
  • Value to mcpx. + complete catalogues from every conformant 2026 server.
  • Effort. S — decode nextCursor as *string.
  • Risk. If not fixed: silent truncation of an upstream’s tools, resources or prompts.
  • Detail. mcpx’s server treats an incoming cursor: "" as “start” (internal/mcpserver/server.go:1483) — harmless, since it never issues one.
  • Sources. 2026-07-28/server/utilities/pagination.mdx:106 “non-null value was provided (e.g. an empty string is a valid cursor and”; internal/mcpclient/client.go:744 “if res.NextCursor == “; v1:packages/opencode/src/mcp/catalog.ts:29

PG-03 invalid cursors

  • What. An invalid cursor SHOULD produce -32602 (Invalid params). The 2026 schema’s InvalidParamsError doc names “invalid or expired cursor values” and ships an example.
  • Where. All five (SHOULD).
  • mcpx @ 05c78b2. An undecodable cursor is ignored and the list restarts at offset 0 (internal/mcpserver/server.go:1483-1486).
  • Value to mcpx. + clients notice a stale cursor instead of looping or duplicating.
  • Effort. S.
  • Risk. Low. In 2026 an error is also the signal for a client to discard its cached pages (PG-11); a silent restart never sends it.
  • Detail. None beyond the above.
  • Sources. 2024-11-05/server/utilities/pagination.mdx:94 “Invalid cursors SHOULD result in an error with code -32602 (Invalid params).”; 2026-07-28/server/utilities/pagination.mdx:111; schema/2026-07-28/schema.ts:368; internal/mcpserver/server.go:1483-1486

PG-04 stable cursors

  • What. Servers SHOULD provide stable cursors. 2026’s caching page adds that there is no cross-page consistency guarantee: if data changes between pages, clients may see duplicates or gaps, and those wanting a snapshot SHOULD re-fetch from the start.
  • Where. All five (SHOULD); the relaxation is 2026.
  • mcpx @ 05c78b2. Cursors are plain offsets, not tied to a list version, so a list that changes between pages (an upstream reload, an adapter added) shifts silently (internal/mcpserver/server.go:1477-1505).
  • Value to mcpx. + low: tools/list fits one page today (61 < 100).
  • Effort. S — version or sign the cursor.
  • Risk. Low, and within what 2026 explicitly tolerates.
  • Detail. mcpx’s own tool list never changes after it is built (TOOL-24, CAP-20), so only resource and prompt lists can shift.
  • Sources. 2024-11-05/server/utilities/pagination.mdx:80 “- Provide stable cursors”; 2026-07-28/server/utilities/caching.mdx:158 “- There is no cross-page consistency guarantee. If the underlying data changes between”

PG-05 page size

  • What. The server decides the page size; clients MUST NOT assume a fixed one.
  • Where. All five.
  • mcpx @ 05c78b2. mcp.pageSize (internal/cli/serve.go:309), default "100" written inline in the settings registry (internal/settings/registry.go:417), with a second inline return 100 fallback (internal/mcpserver/server.go:1469). All four list methods use it.
  • Value to mcpx. + low.
  • Effort. S — move both defaults into defaults.json.
  • Risk. Low. The page size is configurable; only its two defaults are inline constants, against mcpx’s “nothing hardcoded” rule.
  • Detail. With opencode v2’s 64-page cap (PG-06), 100 per page allows 6,400 entries before v2 stops reading.
  • Sources. 2024-11-05/server/utilities/pagination.mdx:18 “- Page size is determined by the server, and clients MUST NOT assume a fixed page”; internal/cli/serve.go:309 “srv.PageSize = a.Settings().Int(”; internal/settings/registry.go:417; internal/mcpserver/server.go:1469 “return 100”

PG-06 client page-following caps

  • What. No revision caps how many pages a client follows; clients choose a cap to survive a server that never stops.
  • Where. opencode v1 pages itself up to 1,000 pages and errors on a repeated cursor; opencode v2 relies on its SDK, which aggregates up to 64 pages.
  • mcpx @ 05c78b2. ListTools, ListResources and ListPrompts follow at most 100 pages (internal/mcpclient/client.go:734, internal/mcpclient/client.go:756, internal/mcpclient/client.go:807), then return what they have without an error, and do not detect a repeated cursor.
  • Value to mcpx. + low.
  • Effort. S — report truncation, detect loops, move the literal into settings.
  • Risk. A server that repeats a cursor costs 100 round trips; a very large one is truncated without notice.
  • Detail. The cap is an inline 100 in three loops.
  • Sources. internal/mcpclient/client.go:734 “for i := 0; i < 100; i++ {”; v1:packages/opencode/src/mcp/catalog.ts:12 “const MAX_LIST_PAGES = 1_000”; v1:packages/opencode/src/mcp/catalog.ts:30; https://unpkg.com/@modelcontextprotocol/client@2.0.0/dist/index.mjs L2904

PG-07 mcpx client swallows list errors

  • What. How a list reader treats an error mid-listing.
  • Where. mcpx client.
  • mcpx @ 05c78b2. ListPrompts returns whatever it has on any error, as if the server had no prompts (internal/mcpclient/client.go:813-818). ListResources requests resources/templates/list once, without a cursor, and ignores its error (internal/mcpclient/client.go:772); ListResourceTemplates does not page either (internal/mcpclient/client.go:556-562).
  • Value to mcpx. + low.
  • Effort. S — distinguish -32601 from other errors; follow template cursors.
  • Risk. A transient error hides a server’s prompts until the next refresh, because the empty list is cached (PG-13); templates past the first page never appear.
  • Detail. The pool calls these only when the server declared the capability, so “no prompts” is not the likely meaning of an error there.
  • Sources. internal/mcpclient/client.go:813-818 “A server without prompts answers method-not-found”; internal/mcpclient/client.go:772; internal/mcpclient/client.go:556-562

PG-08 which results must carry cache hints

  • What. In 2026, complete results of server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read MUST carry ttlMs and cacheScope. prompts/get, completion/complete, tools/call and any input_required result do not. A cached response is keyed by the method plus the parameters that affect it (uri, cursor).
  • Where. 2026 only. The changelog lists five methods and omits server/discover; the caching page and the schema (DiscoverResult extends CacheableResult) include it.
  • mcpx @ c58507a. Both halves landed. As a server, mcpx stamps ttlMs and cacheScope on every complete cacheable result for modern peers (envelope in internal/mcpserver/server.go, scope table and Cache values from the protoMessages list/read max-age settings; legacy peers have both fields stripped, internal/mcpserver/revisions.go). As a client, the list result types carry ttlMs/cacheScope (internal/mcpclient/client.go) and the List*WithHints variants aggregate them across pages into CacheHints (minimum declared TTL; private if any page is private) – #1601 Phase 1, PR #1608.
  • Value to mcpx. + schema-required; hosts could cache mcpx’s small, fixed tool surface.
  • Effort. S — stamp both fields for modern peers alongside resultType. (Done.)
  • Risk. Strict 2026 clients reject the results as schema-invalid.
  • Detail. resources/read is cacheable while prompts/get is not — an asymmetry the schema makes explicit.
  • Sources. 2026-07-28/server/utilities/caching.mdx:13 “Servers MUST include caching hints on results with”; 2026-07-28/changelog.mdx:36 “Require ttlMs and cacheScope fields on results returned by”; schema/2026-07-28/schema.ts:678 “export interface DiscoverResult extends CacheableResult {”; schema/2026-07-28/schema.ts:1779 “export interface ListToolsResult extends PaginatedResult, CacheableResult {”; schema/2026-07-28/schema.ts:1229 “export interface ReadResourceResult extends CacheableResult {”; schema/2026-07-28/schema.ts:1634 “export interface GetPromptResult extends Result {”; 2026-07-28/server/utilities/caching.mdx:31

PG-09 ttlMs

  • What. An integer number of milliseconds the result may be considered fresh, like Cache-Control: max-age. Servers MUST send a value >= 0. 0 means immediately stale; absent means clients assume 0; negative is treated as 0. TTL is not a polling interval; clients that poll MUST add jitter and backoff. A list_changed notification invalidates a fresh cached list.
  • Where. 2026 only.
  • mcpx @ c58507a. Emitted by the server (Cache.List covers discover and the list methods, Cache.Read covers resources/read; a negative configured value is clamped to 0). Honoured by the client for modern upstreams: Pool.refreshSchemas sets the catalogue deadline to fetch time + the minimum effective TTL across the lists actually fetched, where a list that declared no ttlMs – or 0, or a negative – makes the catalogue stale immediately; Pool.Schemas re-lists once the deadline passes. Legacy upstreams keep a zero deadline: their cache never expires on a clock (#1601 Phase 1, PR #1608).
  • Value to mcpx. + mcpx’s own tool list is fixed for the daemon’s life, so a long TTL is honest for tools/list; ttlMs: 0 is always legal where freshness is unknown.
  • Effort. S.
  • Risk. As PG-08.
  • Detail. A server MAY send ttlMs without listChanged, relying on TTL alone.
  • Sources. schema/2026-07-28/schema.ts:1094 “ttlMs: number;”; 2026-07-28/server/utilities/caching.mdx:56 “- If ttlMs is absent, clients SHOULD assume a default of 0 (immediately stale)”; 2026-07-28/server/utilities/caching.mdx:60 “Servers MUST provide a ttlMs value that is >= 0.”; 2026-07-28/server/utilities/caching.mdx:81

PG-10 cacheScope

  • What. "public": no user-specific data; any client, gateway or proxy MAY share it across users. "private": reuse only within the same authorization context; caches MUST NOT be shared across contexts. Servers MUST NOT rely on cacheScope for access control.
  • Where. 2026 only.
  • mcpx @ c58507a. Emitted by the server per method (scope table in internal/mcpserver/server.go). Honoured by the client at the disk boundary (#1601 Phase 2, PR #1705): Registry.SaveCache never persists a private catalogue, public entries are written with their absolute deadline and scope, and loadCache never seeds a private entry – a public entry whose deadline already passed seeds expired, so the first read re-lists. In memory, one private page marks the whole aggregated list private.
  • Value to mcpx. + "private" is the honest answer for a per-user daemon’s resources; "public" fits mcpx’s own tool list, which is identical for every caller. As a gateway in front of several hosts mcpx must honour "private" from upstreams by keying any cache on the upstream credential.
  • Effort. S to emit; M to honour upstream scopes in a shared cache.
  • Risk. High if gotten wrong: "public" on a per-user result lets a shared cache leak one user’s resources to another.
  • Detail. The spec’s own guidance: "public" for lists identical for all users, "private" for user-dependent resources/read results and per-user filtered lists.
  • Sources. schema/2026-07-28/schema.ts:1109 “cacheScope: “; 2026-07-28/server/utilities/caching.mdx:101; 2026-07-28/server/utilities/caching.mdx:178 “- MUST apply appropriate per-primitive access controls, and MUST NOT rely on”

PG-11 caching paginated lists

  • What. Each page is cached independently: its own ttlMs and freshness clock (servers MAY vary TTL per page), all pages of one list MUST share one cacheScope, an expired page is re-fetched by its cursor, and a cursor that becomes invalid means discarding every cached page and starting over.
  • Where. 2026 only.
  • mcpx @ c58507a. Satisfied by construction, in the conservative direction: mcpx’s client aggregates a list’s pages into a single catalogue cache, so there are no per-page clocks to keep – the aggregate takes the minimum TTL declared on any page and is private if any page is private, and an expired catalogue is re-fetched whole (all pages) rather than page by page. This is why the conformance gap caching-page-refetch was retired as not applicable by construction (#1604). mcpx’s silent restart on a bad cursor (PG-03) remains a server-side concern for its own caching clients.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. None beyond the above.
  • Sources. 2026-07-28/server/utilities/caching.mdx:153 “- Each page response carries its own ttlMs value. The freshness clock for each page”; 2026-07-28/server/utilities/caching.mdx:162; 2026-07-28/server/utilities/caching.mdx:166 “Servers MUST apply the same cacheScope to all response pages for a given list”

PG-12 MRTR retries are never cached

  • What. input_required results carry no cache hints, and results of retried requests — those carrying inputResponses or requestState — MUST NOT be cached, because they depend on inputs outside the cache key.
  • Where. 2026 only.
  • mcpx @ c58507a. ✓ — mcpx caches no MRTR result in either direction, and the server stamps cache hints only when the result’s resultType is "complete", so an input_required result carries none.
  • Value to mcpx. + a guardrail for any future cache.
  • Effort. S.
  • Risk. None today.
  • Detail. A resources/read that went through an MRTR round is cacheable by method but not by this rule.
  • Sources. 2026-07-28/server/utilities/caching.mdx:23-25; 2026-07-28/server/utilities/caching.mdx:36 “is, requests carrying inputResponses or requestState—MUST NOT be cached,”

PG-13 mcpx’s upstream schema cache ignores ttlMs

  • What. A 2026 client MAY treat an upstream list as fresh for ttlMs and SHOULD re-fetch once stale.
  • Where. 2026.
  • mcpx @ c58507a. Fixed for the modern era by #1601 (Phases 1-2, PRs #1608 and #1705): the client’s list result types carry the hint fields, the pool cache expires at the earliest declared deadline, and the disk cache is scope-aware (PG-10). Legacy upstreams are unchanged by design – the TTL rules exist only in 2026-07-28, and legacy’s invalidation channel is its notifications. A list_changed notification empties the cache in both eras (internal/daemon/hooks.go OnListChanged -> Pool.Invalidate), now covered by daemon.TestListChangedInvalidatesPoolCache (#1604), which retired the conformance gap caching-notification-invalidates. Two sibling gaps were retired with reasons in the same pass: caching-may-refetch-early-serve-stale – declined by decision on #1601 (refresh is synchronous on read; stale is served only when a refetch of an expired entry fails, #1603), and caching-key-method-params – not applicable by construction (the pool cache is one catalogue per configured server, fetched by fixed list methods; it is never keyed by method+params and resources/read results are not cached by the pool).
  • Value to mcpx. + fresher catalogues from modern upstreams, which today send mcpx no list_changed at all because it never opens a listen stream (notifications register).
  • Effort. S.
  • Risk. Stale catalogues until mcpx refresh.
  • Detail. cacheScope: "private" would matter only if mcpx shared one cache across authorization contexts; the cache is per configured server, and each server carries one credential.
  • Sources. internal/pool/pool.go:446-455; internal/mcpclient/client.go:131-134; 2026-07-28/changelog.mdx:36

PG-14 lists must not vary per connection

  • What. tools/list, prompts/list and resources/list MAY change over time but MUST NOT vary per connection or as a side effect of other requests; they MAY vary by the authorization presented on the request.
  • Where. 2026 only.
  • mcpx @ 05c78b2. mcpx’s tool list is built once per daemon (internal/cli/serve_ask.go:242) and resource and prompt listings come from per-server schema caches (internal/pool/pool.go:121-128), so no list depends on which connection asks.
  • Value to mcpx. + keeps mcpx cacheable; rules out “load a namespace’s tools into this session after the agent asks for it”, a pattern mcpx should not adopt over MCP.
  • Effort. S (policy).
  • Risk. Per-connection lazy tool expansion would break 2026 caching.
  • Detail. This is the list half of the 2026 statelessness change; the ordering half is TOOL-23 in the tools register.
  • Sources. 2026-07-28/server/tools.mdx:65 “but MUST NOT vary”; 2026-07-28/server/tools.mdx:67 “MAY vary by the authorization presented on the request”; 2026-07-28/server/prompts.mdx:60; 2026-07-28/server/resources.mdx:77; internal/cli/serve_ask.go:242

Plugin APIs (opencode v1 and v2) and harness integration

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Every opencode v1 vs v2 plugin-API difference, the state of mcpx's opencode plugin under each, and how code execution meets the harness.

opencode v2 replaces the v1 plugin API wholesale: a new package name, a {id, setup} module instead of a function that returns hooks, a Context of domains instead of an SDK client, and a hook set in which shell.env no longer carries a session id. mcpx’s plugin is v1-only and uses four hooks (shell.env PLG-13, experimental.chat.system.transform PLG-16, the tool map PLG-26, tool.execute.after PLG-24); none of them works unchanged under v2, and one has never worked under v1. The more robust v2 route for session identity is not a plugin at all: v2 sends _meta["ai.opencode/sessionID"] on every MCP call (the _meta and process-model areas), and v2’s code mode will wrap mcpx itself unless mcpx is configured codemode: false.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
PLG-01Plugin package @opencode-ai/plugin renamed @opencode/pluginopencode v1 has opencode v2 has mcpx missingopencode v1 @opencode-ai/plugin · v2 @opencode/plugin✗ — both files import the v1 name (#47)+ highShigh
PLG-02Module shape: hooks-returning function vs {id, setup|effect}opencode v1 has opencode v2 has mcpx missingopencode v1 function or {id?, server} · v2 {id, setup|effect}✗ — v1 shape; v2 rejects the file (#47)+ highMhigh
PLG-03v1 legacy loader calls every export of a plugin fileopencode v1 has has better replacementopencode v1 ✓ · v2 default export only✓ — helpers kept in a subdirectory+ lowSmed
PLG-04Plugin discovery: flat file glob vs files or directoriesopencode v1 has opencode v2 hasopencode v1 {plugin,plugins}/*.{ts,js} · v2 + directoriespartial — mcpx/ subdirectory becomes a package in v2+ lowSmed
PLG-05Config key plugin vs plugins; where options arriveopencode v1 has opencode v2 hasopencode v1 plugin · v2 plugins + legacy pluginpartial — config carries over, module does not+ lowSlow
PLG-06v1 also runs the v2 plugin hostopencode v1 hasopencode v1 ✓n/a — mcpx ships the v1 shape+ lowSlow
PLG-07Plugin runtime: Bun vs build-dependentopencode v1 has opencode v2 hasopencode v1 Bun · v2 Bun build, Node build existspartial — assumes Bun fetch({unix})+ lowSlow
PLG-08Plugin lifecycle: dispose vs setup cleanup and hot reloadopencode v1 has opencode v2 hasopencode v1 dispose · v2 cleanup + file-watch reloadn/a — no dispose used+ lowSlow
PLG-09Plugin instance per directory vs per Locationopencode v1 has opencode v2 hasopencode v1 per directory · v2 per Locationpartial — “session” cache is per place+ medSmed
PLG-10client SDK and serverUrl in plugin inputopencode v1 has opencode v2 hasopencode v1 ✓ · v2 absent (domains instead)✗ — uses client.session.get, client.tui.showToast+ medMhigh
PLG-11directory/worktree/project vs ctx.locationopencode v1 has opencode v2 hasopencode v1 fields · v2 ctx.locationpartial — destructures v1 fields+ lowSlow
PLG-12Bun $ shell handed to pluginsopencode v1 has has no replacementopencode v1 ✓ · v2 —✓ — uses execFile and the socket+ lowSlow
PLG-13shell.env hook vs shell.hook("create.before") without sessionopencode v1 has opencode v2 has mcpx missingopencode v1 sessionID, callID · v2 neither✗ — MCPX_SESSION_ID lost under v2 (#47)+ highMhigh
PLG-14v1 shell.env has no session id on PTY terminalsopencode v1 hasopencode v1 bash tool and ! shell ✓ · PTY —partial — terminal-run mcpx gets no session+ lowSlow
PLG-15v2 per-session environment route replaces the whole envopencode v2 hasopencode v2 only; TUI-owned; in-memoryn/a — docs propose it; it would break shells− harmfulShigh
PLG-16experimental.chat.system.transform output is {system: string[]}opencode v1 has mcpx hasopencode v1 ✓ · v2 no hook of that name✓ fixed — pushes the text onto output.system (#1938; regression test plugin/opencode/mcpx/instructions.test.ts)+ medSmed
PLG-17v2 session.hook("context") edits system, messages, toolsopencode v2 hasopencode v1 three hooks · v2 one; invented tools droppedn/a — not used+ medSmed
PLG-18chat.message vs session.hook("prompt")opencode v1 has opencode v2 hasopencode v1 chat.message · v2 promptn/a — not used+ lowSlow
PLG-19chat.params/chat.headers vs context.options/model.request.headersopencode v1 has opencode v2 hasopencode v1 two hooks · v2 context, model.request (+ v2-only)n/a — not used+ lowSlow
PLG-20experimental.session.compacting vs session.hook("compaction")opencode v1 has opencode v2 hasopencode v1 hook · v2 compactionn/a — not used+ lowSlow
PLG-21v1 hooks with no v2 hook of the same purposeopencode v1 hasopencode v1 onlyn/a — mcpx uses none+ lowSlow
PLG-22event hook vs ctx.event.subscribe() streamopencode v1 has opencode v2 hasopencode v1 pushed · v2 subscribedn/a — not used+ lowSlow
PLG-23tool.execute.before: v2 adds session and mutable inputopencode v1 has opencode v2 hasopencode v1 {args} · v2 {sessionID, agent, messageID, id, input}n/a — not used+ medMmed
PLG-24tool.execute.after: success-only vs status unionopencode v1 has opencode v2 has has better replacementopencode v1 success only · v2 completed or errorpartial — reads title, undefined for MCP+ lowSlow
PLG-25permission.ask declared but dead vs live evaluate hookopencode v1 has opencode v2 has has better replacementopencode v1 never triggered · v2 liven/a — not used+ lowSlow
PLG-26Defining a tool: zod map vs ctx.tool.transform editoropencode v1 has opencode v2 hasopencode v1 map, un-namespaced · v2 editor, optional namespacepartial — v1 map only+ highMmed
PLG-27v2 plugin tools hide behind code mode by defaultopencode v2 has mcpx missingopencode v2 codemode !== false · v1 plugin tools direct✗ — no v2 port; it would need codemode: false (#47)+ highShigh
PLG-28A plugin tool can ask the useropencode v1 has has no replacementopencode v1 ToolContext.ask · v2 —partial — mcpx_daemon_select relies on it− lostMmed
PLG-29Toast from a server pluginopencode v1 has opencode v2 hasopencode v1 client.tui.showToast · v2 TUI plugin onlypartial — v1 toast only+ medMmed
PLG-30TUI plugin shape and dialogsopencode v1 has opencode v2 has mcpx missingopencode v1 {id?, tui} JSX · v2 {id, setup} promises✗ — v1 TUI plugin only (#47)+ medMlow
PLG-31ctx.rpc between server and TUI pluginsopencode v2 hasopencode v1 — · v2 ✓n/a — v1 uses a file as the channel+ medMlow
PLG-32Plugin storageopencode v1 has opencode v2 hasopencode v1 TUI kv only · v2 ctx.storagen/a — own remember file+ lowSlow
PLG-33Calling a model from a pluginopencode v1 has opencode v2 hasopencode v1 full turn · v2 ctx.generate.textn/a — not examined+ medMlow
PLG-34Plugins add or change MCP servers (Location-scoped)opencode v2 hasopencode v1 config hook · v2 ctx.mcp.transform/reload/listn/a — not used+ medSmed
PLG-35?codemode=false appended to remote MCP URLsopencode v2 has mcpx honours, does not sendopencode v2 client◐ — honoured on /mcp (raw tools, namespaced); mcpx as a client still does not send it (#1940)+ medMmed
PLG-36Hooks and permission checks per child call in code modeopencode v1 has opencode v2 has mcpx missingopencode v1/v2 per child · mcpx one opaque call✗ — script calls bypass opencode permissions (#218)+ highLhigh
PLG-37Live status of each child call during a scriptopencode v1 has opencode v2 has mcpx missingopencode v1 metadata · v2 progress✗ — plugin makes one blocking POST (#218)+ medSlow
PLG-38Cancelling in the harness stops the scriptopencode v1 has opencode v2 has mcpx missingopencode v1 abort signal · v2 fiber interrupt✗ — plugin ignores the abort signal (#218)+ medSmed
PLG-39Harness session and working directory reach the scriptopencode v2 has mcpx missingopencode v2 _meta on child calls · mcpx bash path only✗ — plugin mcpx_exec sends neither (#189)+ highShigh
PLG-40Plugin mcpx_exec reads a field the daemon never returnsmcpx missingmcpx plugin only✗ broken — whole envelope shown to model (#189)+ medSmed
PLG-41Code execution offered as an MCP servermcpx has, others don'tmcpx /mcp · Cloudflare codeMcpServer · opencode — · lootbox —✓ — mcpx_exec and friends over /mcp+ medSlow

PLG-01 Plugin package @opencode-ai/plugin renamed @opencode/plugin

  • What. The plugin SDK changed npm scope. v1 exports ., ./tool, ./tui, ./v2/effect, ./v2/promise; v2 exports ., ./effect, ./host, ./tui. v2 has no @opencode-ai/plugin anywhere in core, cli, server or plugin sources.
  • Where. opencode v1 and v2, incompatible.
  • mcpx @ 05c78b2. Both plugin files import the v1 name (plugin/opencode/mcpx-session.ts:1, plugin/opencode/mcpx-tui.tsx:42).
  • Value to mcpx. + high: the v2 port is a new file, not an edit.
  • Effort. S for the import; the API behind it differs everywhere (PLG-02 onward).
  • Risk. High: under v2 the v1 plugin cannot even resolve its import.
  • Detail. v1 auto-installs @opencode-ai/plugin@<version> into every config directory so imports resolve; no equivalent auto-install of @opencode/plugin was found in v2, whose loader takes npm packages via npm.add and local files via a watching source loader. How a local v2 plugin file resolves the import in the Bun-compiled binary is unverified (v2’s Node entry publishes the API on a global symbol).
  • Sources. v1:packages/plugin/package.json:3 “"name": "@opencode-ai/plugin"”; v2:packages/plugin/package.json:3 “"name": "@opencode/plugin"”; plugin/opencode/mcpx-session.ts:1 “import { tool, type Plugin } from "@opencode-ai/plugin"”; v1:packages/opencode/src/config/config.ts:456; v2:packages/core/src/plugin/module.ts:91; v2:packages/cli/src/node/plugin-runtime.promise.ts:14

PLG-02 Module shape: hooks-returning function vs {id, setup|effect}

  • What. v1 calls Plugin = (input, options) => Promise<Hooks> and uses the hooks object it returns. v2 requires a default export {id, setup(ctx)} (Promise) or {id, effect(ctx)} (Effect) that registers hooks imperatively; setup returns an optional cleanup.
  • Where. v1: plain function export or PluginModule {id?, server, tui?: never}. v2: only the {id, setup|effect} union; anything else is a PluginModule.LoadError.
  • mcpx @ 05c78b2. export default (async ({ directory, worktree, project, client }, options) => {…}) satisfies Plugin (plugin/opencode/mcpx-session.ts:169), v1 only.
  • Value to mcpx. + high: a v2 build is required before mcpx’s plugin works on v2. − two artifacts to maintain.
  • Effort. M — every hook maps to a different v2 domain call.
  • Risk. High: v2 rejects the current file with “Plugin must export a default definition with an id and an effect or setup function.”
  • Detail. v2 validates with Effect Schema Struct({id: String, effect|setup: function}); id is mandatory and duplicate ids fail (v2:packages/core/src/plugin/supervisor.ts:108).
  • Sources. v1:packages/plugin/src/index.ts:74 “export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise”; v2:packages/core/src/plugin/module.ts:60; v2:packages/core/src/plugin/module.ts:111; v2:packages/plugin/src/promise/plugin.ts:58; v2:packages/plugin/src/promise/plugin.ts:60

PLG-03 v1 legacy loader calls every export of a plugin file

  • What. When a v1 module’s default is not a {id|server|tui} object, every export is treated as a plugin function; one non-function export throws and the whole module is skipped (logged).
  • Where. v1 only; v2 reads only default.
  • mcpx @ 05c78b2. Works around it by keeping helpers in a subdirectory and exporting only the default (plugin/opencode/mcpx-session.ts:2).
  • Value to mcpx. + low: the workaround is still needed for v1.
  • Effort. S.
  • Risk. Med: a helper added beside mcpx-session.ts silently disables the plugin in v1.
  • Detail. The throw happens in getLegacyPlugins before any export is invoked, so there is no partial registration.
  • Sources. v1:packages/opencode/src/plugin/index.ts:107 “if (!plugin) throw new TypeError("Plugin export is not a function")”; v1:packages/opencode/src/plugin/index.ts:123; v2:packages/core/src/plugin/module.ts:115

PLG-04 Plugin discovery: flat file glob vs files or directories

  • What. v1 scans {plugin,plugins}/*.{ts,js} in each config directory, non-recursively. v2 lists plugin/ and plugins/ entries and accepts .ts/.js files and directories (package-style, resolved through server, index, tui or rpc entrypoints).
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Relies on the non-recursive glob to hide its mcpx/ helper directory (plugin/opencode/README.md:34).
  • Value to mcpx. + low: v2 lets one directory ship server, TUI and rpc together.
  • Effort. S.
  • Risk. Med: copying the v1 layout into a v2 plugin directory makes v2 try to load mcpx/ as a plugin package and fail (“Plugin entrypoint not found”).
  • Detail. v2 Host.resolve looks for server, then "" (index), tui, rpc.
  • Sources. v1:packages/opencode/src/config/plugin.ts:21 “Glob.scan("{plugin,plugins}/*.{ts,js}"”; v2:packages/core/src/plugin/source-directory.ts:25 “if (entry.type === "directory") return Option.some(entry.target)”; v2:packages/plugin/src/host.ts:43

PLG-05 Config key plugin vs plugins; where options arrive

  • What. v1 config is plugin: Array<string | [string, options]>; v2’s native key is plugins: [{package, options}], and it still accepts legacy plugin entries, normalising both into plugins. Options reach a v1 plugin as its second argument and a v2 plugin as ctx.options.
  • Where. Both accept plugin; only v2 has plugins.
  • mcpx @ 05c78b2. The README documents "plugin": [["./…/mcpx-session.ts", {…}]] (plugin/opencode/README.md:53).
  • Value to mcpx. + low: configuration is the one thing that carries across.
  • Effort. S.
  • Risk. Low: a user who keeps the v1 entry on v2 gets a module load error (PLG-02), not a config error.
  • Detail. None beyond the above.
  • Sources. v1:packages/plugin/src/index.ts:71 “plugin?: Array<string | [string, PluginOptions]>”; v2:packages/core/src/config/normalize.ts:185; v2:packages/core/src/config/normalize.ts:190; v2:packages/plugin/src/promise/plugin.ts:29

PLG-06 v1 also runs the v2 plugin host

  • What. v1.18.31 contains packages/core and wires its location services, including PluginV2.node and a config-plugin loader that globs the same {plugin,plugins}/*.{ts,js} and accepts {id, effect|setup}.
  • Where. v1 only (v2 is the host).
  • mcpx @ 05c78b2. Not affected; mcpx ships the v1 shape.
  • Value to mcpx. + low: a v2-shaped plugin can be tried under v1. − a v2 plugin in a shared directory is also offered to the v1 loader, which rejects it (“must default export an object with server()”), logged and non-fatal.
  • Effort. S.
  • Risk. Low: log noise.
  • Detail. Whether v1 sessions consume v2 hooks such as session.hook("context") is unverified: the host is wired, but v1’s prompt path uses v1 triggers (v1:packages/opencode/src/session/llm/request.ts:69).
  • Sources. v1:packages/core/src/location-services.ts:52 “PluginV2.node,”; v1:packages/core/src/config/plugin/external.ts:60; v1:packages/core/src/plugin/internal.ts:119; v1:packages/opencode/src/plugin/shared.ts:297

PLG-07 Plugin runtime: Bun vs build-dependent

  • What. v1 plugins run under Bun (the server hands in Bun.$). v2’s shipped build is Bun (its source loader uses Bun.Transpiler), but v2 also has a Node build target. In both, plugins are imported unsandboxed into the server process (v1 the server Worker, v2 the daemon).
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. The plugin reaches the daemon socket with Bun’s fetch(url, {unix}); its own comment notes that Node’s fetch ignores the option (plugin/opencode/mcpx/daemon.ts:348-351).
  • Value to mcpx. + low: holds for the nix build installed here.
  • Effort. S.
  • Risk. Low: a Node build of v2 would break unix-socket fetch.
  • Detail. docs/opencode-plugin.md:201 says fetch(url, {unix}) “works in both, since plugins run under Bun”; the source says that depends on the build (v2:packages/cli/package.json:21).
  • Sources. v1:packages/opencode/src/plugin/index.ts:167; v2:packages/plugin/src/source.bun.ts:27; v2:packages/cli/package.json:21 “"build:node": "bun run script/build-node.ts",”; v1:packages/opencode/src/plugin/index.ts:118; v2:packages/core/src/plugin/module.ts:102; docs/opencode-plugin.md:201

PLG-08 Plugin lifecycle: dispose vs setup cleanup and hot reload

  • What. v1 plugins may return a dispose hook, run on the instance finalizer. v2 setup may return a cleanup, and local plugin sources (and their local imports) are file-watched and reloaded on change.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. No dispose used.
  • Value to mcpx. + low: v2 reload makes iteration fast.
  • Effort. S.
  • Risk. Low: module-level state (mcpx’s sessions cache, the chosen daemon) resets on every v2 reload.
  • Detail. v2’s Bun source loader scans imports with Bun.Transpiler to decide what to watch.
  • Sources. v1:packages/plugin/src/index.ts:223 “dispose?: () => Promise”; v2:packages/plugin/src/promise/plugin.ts:60; v2:packages/core/src/plugin/module.ts:22; v2:packages/plugin/src/source.bun.ts:27

PLG-09 Plugin instance per directory vs per Location

  • What. v1 instantiates each plugin once per directory instance; v2 once per Location (directory plus workspace id). Neither is per session.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Caches per-session data inside one plugin instance (plugin/opencode/mcpx-session.ts:375).
  • Value to mcpx. + med: in-memory “session” state for the daemon choice is really “this directory in this process”.
  • Effort. S.
  • Risk. Med: in v2 one plugin instance serves every TUI window attached to the daemon, so that memory is shared across windows (process-model area).
  • Detail. v1’s event hook only receives events whose location.directory equals the instance directory.
  • Sources. v1:packages/opencode/src/plugin/index.ts:134; v1:packages/opencode/src/plugin/index.ts:256; v2:packages/core/src/plugin.ts:296 “Node.makeLocationNode({”

PLG-10 client SDK and serverUrl in plugin input

  • What. v1 hands a plugin an SDK client bound to the server (through an in-process fetch shim when no port is open) and a serverUrl getter. v2’s Context has neither; it has typed domains instead (app, location, session, mcp, tool, shell, permission, generate, storage, rpc, event and more).
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Uses client.session.get to describe sessions (plugin/opencode/mcpx-session.ts:393) and client.tui.showToast for its boot and ambiguity toast (plugin/opencode/mcpx-session.ts:298).
  • Value to mcpx. + med: v2 domains are typed and direct (ctx.session.get). − every client call must be re-sourced, and the toast has no server-side equivalent (PLG-29).
  • Effort. M.
  • Risk. High: the v1 code paths do not exist on v2.
  • Detail. v2 also removes the TUI routes from its HTTP protocol, so there is no URL a plugin could call instead.
  • Sources. v1:packages/plugin/src/index.ts:56; v1:packages/opencode/src/plugin/index.ts:146; v1:packages/opencode/src/plugin/index.ts:163; v2:packages/plugin/src/promise/plugin.ts:26; v2:packages/plugin/src/promise/plugin.ts:46

PLG-11 directory/worktree/project vs ctx.location

  • What. v1 passes project, directory and worktree as input fields; v2 carries directory, workspace and project in ctx.location.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Destructures directory, worktree, project, client (plugin/opencode/mcpx-session.ts:169).
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. v1 also passes experimental_workspace; v2 folds workspace into the Location key.
  • Sources. v1:packages/plugin/src/index.ts:58; v2:packages/plugin/src/promise/plugin.ts:28; plugin/opencode/mcpx-session.ts:169

PLG-12 Bun $ shell handed to plugins

  • What. v1 passes Bun.$ as input.$; v2’s Context has no command executor (its shell domain is only the create.before hook).
  • Where. opencode v1 only.
  • mcpx @ 05c78b2. Already independent of $: it spawns with node:child_process execFile (plugin/opencode/mcpx/daemon.ts:41) and talks to the daemon over the unix socket (plugin/opencode/mcpx/daemon.ts:352).
  • Value to mcpx. + low: nothing to port.
  • Effort. S.
  • Risk. Low.
  • Detail. docs/opencode-plugin.md:103 says mcpx’s plugin “uses $ for exactly two things”; the source at 05c78b2 uses execFile and the socket, and does not destructure $. import("node:child_process") still works in v2 because plugins are imported unsandboxed.
  • Sources. v1:packages/plugin/src/index.ts:65 “$: BunShell”; v2:packages/plugin/src/promise/shell.ts:12 “readonly "create.before": ShellCreateBefore”; plugin/opencode/mcpx/daemon.ts:41; docs/opencode-plugin.md:103

PLG-13 shell.env hook vs shell.hook("create.before") without session

  • What. v1 shell.env(input: {cwd, sessionID?, callID?}, output: {env}) runs per command; v2’s create.before event is {command, cwd, timeout, shell, env} with a mutable env and no session or call id.
  • Where. v1 bash tool and ! shell pass sessionID/callID. v2 never exposes them to this hook, although its shell tool carries metadata.sessionID internally.
  • mcpx @ 05c78b2. Sets MCPX_SESSION_ID from input.sessionID (plugin/opencode/mcpx-session.ts:664, :670); this is how mcpx run from an agent’s shell gets a session-scoped browser.
  • Value to mcpx. + high: session attribution is the whole reason mcpx uses the hook.
  • Effort. M. The only v2 hook that sees both sessionID and the command is tool.hook("execute.before") (PLG-23); the robust v2 answer is server-side: read _meta["ai.opencode/sessionID"] when opencode calls mcpx over MCP (_meta area).
  • Risk. High: a naive port injects no session id and session leasing silently degrades to one shared instance.
  • Detail. v2’s create.before env starts from the session environment if one was set (PLG-15), else process.env, plus TERM and OPENCODE_TERMINAL=1.
  • Sources. v1:packages/plugin/src/index.ts:270; v1:packages/opencode/src/tool/shell.ts:418; v2:packages/plugin/src/promise/shell.ts:3; v2:packages/core/src/shell.ts:268 “yield* hooks.trigger("shell", "create.before", invocation)”; v2:packages/core/src/tool/plugin/shell.ts:205 “metadata: { sessionID: context.sessionID },”; v2:packages/plugin/src/promise/tool.ts:40

PLG-14 v1 shell.env has no session id on PTY terminals

  • What. v1 fires shell.env from three places; the PTY paths (the TUI terminal, HTTP pty.create) pass only {cwd}.
  • Where. opencode v1.
  • mcpx @ 05c78b2. The hook comment says it “receives exactly { cwd, sessionID?, callID? }” (plugin/opencode/mcpx-session.ts:44), which is right, but mcpx run inside an opencode terminal gets no MCPX_SESSION_ID.
  • Value to mcpx. + low: explains “unknown” sessions in mcpx logs from terminals.
  • Effort. S (document).
  • Risk. Low.
  • Detail. docs/opencode-plugin.md:411-412 says shell.env carries sessionID and callID; the source passes only cwd on PTY paths.
  • Sources. v1:packages/opencode/src/plugin/pty-environment.ts:18; v1:packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts:71; v1:packages/opencode/src/session/prompt.ts:554; docs/opencode-plugin.md:411

PLG-15 v2 per-session environment route replaces the whole env

  • What. PUT /api/experimental/session/:sessionID/environment {variables} stores a per-session map that replaces process.env for that session’s shell commands. The v2 TUI itself calls it with the terminal’s environment whenever a session view is active.
  • Where. opencode v2 only; stored in an in-memory global map (lost on daemon restart); ignored when the Location has a workspace id.
  • mcpx @ 05c78b2. Not used. docs/opencode-plugin.md proposes it as the v2 replacement for shell.env.
  • Value to mcpx. − as a plugin channel it is unusable: a plugin that PUTs {MCPX_SESSION_ID} wipes PATH and HOME for that session and is overwritten by the TUI’s next update; and a v2 plugin has no HTTP client to call it with.
  • Effort. S (to drop the plan).
  • Risk. High if adopted: shell commands break.
  • Detail. docs/opencode-plugin.md:86 and :412 call it “the API v2 added for this”; the route’s own description is “Replace the process environment used by local shell commands for this session.”
  • Sources. v2:packages/protocol/src/groups/session.ts:795; v2:packages/core/src/shell.ts:263 “…(sessionEnvironment ?? process.env),”; v2:packages/core/src/shell.ts:254; v2:packages/core/src/session/environment.ts:18; v2:packages/tui/src/app.tsx:511; docs/opencode-plugin.md:86

PLG-16 experimental.chat.system.transform output is {system: string[]}

  • What. v1 triggers the hook with output = { system }, an array of strings. mcpx’s handler read output.parts, found nothing, and returned, so the plugin’s instructions option never injected anything. Fixed by #1938: the handler now pushes the text onto output.system, with a regression test against v1’s real call shape.
  • Where. opencode v1 (both trigger sites pass {system}); v2 has no hook of this name.
  • mcpx @ 05c78b2. const parts = (output as any)?.parts; if (Array.isArray(parts)) parts.push({ type: "text", text }) (plugin/opencode/mcpx-session.ts:745-746, hook at :737).
  • Value to mcpx. + med: a one-line fix, output.system.push(text).
  • Effort. S.
  • Risk. Was Med: users who set instructions: true believed the model was told about mcpx; it was not. Resolved by #1938.
  • Detail. v1 request preparation re-joins extra system entries into one message when a plugin pushes more than one (v1:packages/opencode/src/session/llm/request.ts:74). The v2 analogue is session.hook("context"), whose system is Array<SystemPart> ({type: "text", text}): the shape mcpx pushes, on a different key (PLG-17).
  • Sources. v1:packages/plugin/src/index.ts:291; v1:packages/opencode/src/session/llm/request.ts:69; v1:packages/opencode/src/agent/agent.ts:381; plugin/opencode/mcpx-session.ts:745 “const parts = (output as any)?.parts”; v2:packages/plugin/src/promise/session.ts:28 “system: Array”

PLG-17 v2 session.hook("context") edits system, messages, tools

  • What. The v2 context event carries system, messages, options and tools (description and input schema). After the hook, tool entries that match neither an original definition nor an original name are dropped. v1 spreads this over experimental.chat.system.transform, experimental.chat.messages.transform and tool.definition.
  • Where. opencode v2 (one hook); v1 (three hooks, and tool.definition applies only to registry tools, never to MCP tools).
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + med: the right place for a short “mcpx exists” line in v2. − it cannot add a catalogue pseudo-tool.
  • Effort. S.
  • Risk. Med: docs/opencode-plugin.md:348 suggests putting a budgeted catalogue into event.tools; the source drops invented entries.
  • Detail. Tools can be renamed (moved to a new key) and re-described; execution maps back by object identity.
  • Sources. v2:packages/plugin/src/promise/session.ts:35; v2:packages/core/src/session/model-request.ts:220 “// Match by identity first, then by key. Entries matching neither were invented by a”; v2:packages/core/src/session/model-request.ts:361; v1:packages/plugin/src/index.ts:282; v1:packages/opencode/src/tool/registry.ts:318; docs/opencode-plugin.md:348

PLG-18 chat.message vs session.hook("prompt")

  • What. v1’s chat.message hook becomes v2’s session.hook("prompt").
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. Listed in the plugin-API matrix only; the payload shapes were not compared.
  • Sources. v1:packages/plugin/src/index.ts:234; v2:packages/plugin/src/promise/session.ts:114

PLG-19 chat.params/chat.headers vs context.options/model.request.headers

  • What. v1’s per-request LLM parameter and header hooks map to v2 context (mutable options: typed generation keys plus provider options) and model.request (mutable headers, baseURL). v2 adds http.request, http.response, retry, title, generate, compaction and experimental.ws.handshake.
  • Where. opencode v1 and v2, different shapes.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. v2’s SessionRequestKind distinguishes primary | compaction | title | generate.
  • Sources. v1:packages/plugin/src/index.ts:247; v1:packages/plugin/src/index.ts:257; v2:packages/plugin/src/promise/session.ts:115; v2:packages/plugin/src/promise/session.ts:119; v2:packages/plugin/src/promise/session.ts:117; v2:packages/core/src/session/model-request.ts:257

PLG-20 experimental.session.compacting vs session.hook("compaction")

  • What. v1’s compaction hook becomes v2’s session.hook("compaction").
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. v1’s companion experimental.compaction.autocontinue has no v2 hook (PLG-21).
  • Sources. v1:packages/plugin/src/index.ts:305; v2:packages/plugin/src/promise/session.ts:116

PLG-21 v1 hooks with no v2 hook of the same purpose

  • What. experimental.text.complete, experimental.provider.small_model, experimental.compaction.autocontinue, command.execute.before and config have no v2 hook; auth/provider are replaced by integration, provider and model transforms. The v2 hook set is session.{prompt, context, compaction, generate, title, model.request, http.request, http.response, experimental.ws.handshake, retry}, tool.{execute.before, execute.after}, shell.create.before, permission.evaluate, aisdk.{sdk, language}, plus domain transforms.
  • Where. opencode v1 only for the listed names; aisdk hooks are v2-only.
  • mcpx @ 05c78b2. Uses none of them.
  • Value to mcpx. + low: confirms mcpx’s four hooks are the only ones to port.
  • Effort. S.
  • Risk. Low.
  • Detail. v1’s config hook runs once after load and is the only way a v1 plugin can add MCP servers (PLG-34).
  • Sources. v1:packages/plugin/src/index.ts:327; v1:packages/plugin/src/index.ts:297; v1:packages/plugin/src/index.ts:316; v1:packages/plugin/src/index.ts:262; v1:packages/opencode/src/plugin/index.ts:247; v2:packages/plugin/src/README.md:68; v2:packages/plugin/src/promise/session.ts:113; v2:packages/plugin/src/promise/aisdk.ts:5

PLG-22 event hook vs ctx.event.subscribe() stream

  • What. v1 pushes every bus event for the instance directory to hook.event({event}); v2 exposes event.subscribe(), a stream of server and rpc events.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + low: a v2 plugin could watch mcp.* status events to know when mcpx connects.
  • Effort. S.
  • Risk. Low.
  • Detail. v1 fires void hook.event(...) without awaiting it.
  • Sources. v1:packages/opencode/src/plugin/index.ts:259; v2:packages/core/src/plugin/host.ts:256

PLG-23 tool.execute.before: v2 adds session and mutable input

  • What. v1 tool.execute.before(input {tool, sessionID, callID}, output {args}); v2 execute.before {tool, sessionID, agent, messageID, id, input}, where the possibly replaced event.input is what executes, for direct and code-mode calls alike.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + med: the only v2 hook that sees both the session id and a shell command before it runs; a plugin could prefix the shell tool’s command or record sessionID → callID for correlation.
  • Effort. M.
  • Risk. Med: rewriting commands is visible in the transcript; correlating with create.before races under concurrency.
  • Detail. v2’s shell tool is named shell (v2:packages/core/src/tool/plugin/shell.ts:22).
  • Sources. v1:packages/plugin/src/index.ts:266; v2:packages/plugin/src/promise/tool.ts:38; v2:packages/core/src/tool.ts:102; v2:packages/core/src/tool.ts:257 “const event = yield* beforeExecute(input.call.name, input.call.input, context)”

PLG-24 tool.execute.after: success-only vs status union

  • What. v1 fires tool.execute.after(input {tool, sessionID, callID, args}, output) only after a successful execute; for MCP tools output is the raw CallToolResult, not {title, output, metadata}. v2 fires execute.after for both outcomes with status: "completed" (result) or "error" (error).
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. Records timing into mcpx’s log (plugin/opencode/mcpx-session.ts:773) and reads (output as any)?.title (:780), which is always undefined for MCP tools in v1; failures are never recorded.
  • Value to mcpx. + low: v2 lets mcpx record failures.
  • Effort. S.
  • Risk. Low.
  • Detail. v1 errors skip the hook entirely (it is triggered after item.execute returns). In v2 an MCP isError becomes a ToolFailure that does reach this hook with status: "error". Both versions fire it per child call in code mode (PLG-36).
  • Sources. v1:packages/plugin/src/index.ts:274; v1:packages/opencode/src/session/tools.ts:421; v2:packages/plugin/src/promise/tool.ts:46; v2:packages/core/src/tool.ts:131; plugin/opencode/mcpx-session.ts:780 “title: (output as any)?.title,”

PLG-25 permission.ask declared but dead vs live evaluate hook

  • What. v1 declares a "permission.ask" hook that no code triggers; v2 triggers permission/evaluate with a mutable effect and message.
  • Where. v1 declared only; v2 live.
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + low: a v2 plugin could auto-allow mcpx tools.
  • Effort. S.
  • Risk. Low: implementing the v1 hook is a silent no-op.
  • Detail. The only permission.ask( call in v1 is the permission service itself, not a plugin trigger.
  • Sources. v1:packages/plugin/src/index.ts:261; v1:packages/opencode/src/session/processor.ts:372; v2:packages/core/src/permission.ts:174 “const event = yield* hooks.trigger("permission", "evaluate", {”

PLG-26 Defining a tool: zod map vs ctx.tool.transform editor

  • What. v1 plugin tools are a {[name]: ToolDefinition} map with zod raw-shape args (tool.schema = z) returning a string or {title, output, metadata, attachments}. v2 tools are added through ctx.tool.transform(e => e.add({name, options: {namespace, codemode, permission, pinned}, description, input, output, execute})), where input and output are Effect Schema, Standard Schema (zod 4 qualifies) or JSON Schema, and execute returns {output, content, metadata}.
  • Where. opencode v1 map; v2 editor.
  • mcpx @ 05c78b2. v1 map (plugin/opencode/mcpx-session.ts:762) with zod args (:499): mcpx_daemon_*, mcpx_discover, mcpx_exec, mcpx_observe.
  • Value to mcpx. + high: v2 gives typed structured output. − a rewrite.
  • Effort. M.
  • Risk. Med: v2 rejects names outside ^[A-Za-z0-9_-]{1,64}$.
  • Detail. v1 plugin tools are not namespaced (id = map key), while v1 file tools in {tool,tools}/*.{js,ts} get <file>_<export>. v2 namespaces only when options.namespace is set (id ns_name, dots become _). docs/opencode-plugin.md:78 calls v2 tools “namespaced and typed”; the namespace is optional, and without codemode: false the tool disappears into code mode (PLG-27).
  • Sources. v1:packages/plugin/src/tool.ts:45; v1:packages/plugin/src/tool.ts:52 “tool.schema = z”; v1:packages/opencode/src/tool/registry.ts:202; v2:packages/plugin/src/promise/tool.ts:66; v2:packages/core/src/tool/runtime.ts:276; v2:packages/core/src/tool.ts:297; docs/opencode-plugin.md:78

PLG-27 v2 plugin tools hide behind code mode by default

  • What. In v2 every registered tool whose options.codemode !== false is removed from the model’s direct tool list and exposed only inside the execute tool; built-ins opt out one by one with codemode: false.
  • Where. opencode v2 only. v1’s code mode wraps MCP tools only, never plugin tools.
  • mcpx @ 05c78b2. n/a (v1 plugin).
  • Value to mcpx. + high: a v2 port must set options: { codemode: false } on mcpx_daemon_* and mcpx_exec, or they vanish behind execute.
  • Effort. S.
  • Risk. High: tools the model should call directly become code-mode-only functions.
  • Detail. execute is a reserved name for a direct tool. Code mode itself can be switched off only by a wholly-denying permission rule on execute.
  • Sources. v2:packages/core/src/tool.ts:228 “const direct = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode === false))”; v2:packages/core/src/tool.ts:229; v2:packages/core/src/tool.ts:299; v2:packages/core/src/tool/plugin/shell.ts:191 “options: { codemode: false },”

PLG-28 A plugin tool can ask the user

  • What. v1 ToolContext.ask({permission, patterns, always, metadata}) raises a permission prompt. v2’s tool context is {sessionID, agent, messageID, id, progress}, with no ask, and the permission domain offers list, get, reply, rules and the hook, but no ask.
  • Where. opencode v1 only.
  • mcpx @ 05c78b2. mcpx_daemon_select confirms through ctx.ask (plugin/opencode/mcpx-session.ts:529).
  • Value to mcpx. − the only v1 server-side interactive channel is gone.
  • Effort. M — a v2 TUI plugin plus ctx.rpc (PLG-30, PLG-31), or drop the confirmation.
  • Risk. Med.
  • Detail. docs/opencode-plugin.md:272 lists “a tool with a permission prompt (server plugin, v1 and v2)”; v2’s tool context has no ask. v2 plugin tools are gated only by wholly-disabling permission rules on options.permission ?? name.
  • Sources. v1:packages/plugin/src/tool.ts:19 “ask(input: AskInput): Promise”; v1:packages/opencode/src/tool/registry.ts:150; v2:packages/schema/src/tool.ts:14; v2:packages/core/src/tool.ts:225; docs/opencode-plugin.md:272

PLG-29 Toast from a server plugin

  • What. v1 plugins call client.tui.showToast. A v2 server plugin has no client, and the v2 HTTP protocol has no toast route; toasts exist only in the TUI plugin API (ui.toast.show).
  • Where. v1 server plugin: yes. v2 server plugin: no. v2 TUI plugin: yes.
  • mcpx @ 05c78b2. Boot and ambiguity toast via client.tui.showToast (plugin/opencode/mcpx-session.ts:298).
  • Value to mcpx. + med: on v2 ambiguity must go on tool results (already done through annotate) or through a TUI plugin.
  • Effort. M — a v2 TUI plugin plus ctx.rpc.
  • Risk. Med.
  • Detail. docs/opencode-plugin.md:111 and :361 say the server plugin’s client.tui.* is “identical in v1 and v2”; v2’s server context has no client. v2 defines an ephemeral tui.toast.show event schema, but the plugin event domain is subscribe-only.
  • Sources. v1:packages/sdk/js/src/gen/sdk.gen.ts:1118 “public showToast”; v2:packages/core/src/plugin/host.ts:255; v2:packages/plugin/src/tui/context.ts:279 “show(options: ToastOptions): void”; v2:packages/schema/src/tui-event.ts:42; docs/opencode-plugin.md:111

PLG-30 TUI plugin shape and dialogs

  • What. v1 TUI plugins default-export {id?, tui(api, options, meta)} and render api.ui.DialogSelect through api.ui.dialog.replace. v2 TUI plugins default-export {id, setup(ctx)} with promise dialogs (ui.dialog.select<V>(), prompt(), confirm(), alert()), ui.toast.show() and keymap.layer(() => …).
  • Where. opencode v1 and v2, incompatible.
  • mcpx @ 05c78b2. v1 only (plugin/opencode/mcpx-tui.tsx:123, :189, :206).
  • Value to mcpx. + med: v2’s promise dialogs are simpler than JSX dialogs, and a v2 TUI plugin is where toasts and confirmations must now live.
  • Effort. M.
  • Risk. Low today.
  • Detail. v1 forbids server and tui in one module; v2 packages declare server, tui and rpc entrypoints side by side (v2:packages/plugin/src/host.ts:43).
  • Sources. v1:packages/plugin/src/tui.ts:630; v1:packages/plugin/src/tui.ts:604; v2:packages/plugin/src/tui/plugin.ts:7; v2:packages/plugin/src/tui/context.ts:379; v2:packages/plugin/src/tui/context.ts:440

PLG-31 ctx.rpc between server and TUI plugins

  • What. v2 server plugins can rpc.register(definition, handlers) and emit events; a plugin package can ship an rpc entrypoint. It is the only channel a v2 server plugin has to UI.
  • Where. opencode v2.
  • mcpx @ 05c78b2. Uses the filesystem as the channel between the server and TUI plugins (plugin/opencode/mcpx-tui.tsx:17).
  • Value to mcpx. + med: replaces the remember-file hand-off.
  • Effort. M.
  • Risk. Low.
  • Detail. RpcDomain.register returns {events.emit} for push.
  • Sources. v2:packages/plugin/src/promise/rpc.ts:27; v2:packages/core/src/plugin/host.ts:119

PLG-32 Plugin storage

  • What. v1 server plugins have no persistence API (v1 TUI plugins have kv); v2 server plugins have storage.get/set/remove/scan, namespaced per plugin id, and v2 TUI plugins have storage.store/memory.
  • Where. opencode v1 (TUI only) and v2.
  • mcpx @ 05c78b2. Remembers the daemon choice in its own file (plugin/opencode/mcpx-session.ts:455).
  • Value to mcpx. + low: the remember file can go on v2; sharing between TUI and server still needs rpc or a file.
  • Effort. S.
  • Risk. Low.
  • Detail. The namespace is plugin:<hex of id>:.
  • Sources. v1:packages/plugin/src/tui.ts:611 “kv: TuiKV”; v2:packages/plugin/src/promise/storage.ts:4; v2:packages/core/src/plugin/host.ts:450; v2:packages/core/src/plugin/host.ts:578

PLG-33 Calling a model from a plugin

  • What. v1 plugins can only drive an agent turn (client.session.prompt/promptAsync/command). v2 plugins have ctx.generate.text({prompt, model?}) → {text} and ctx.session.generate, also exposed as POST /api/generate.
  • Where. opencode v1 (full turn) and v2 (one-shot).
  • mcpx @ 05c78b2. Not examined (the sampling area covers what mcpx forwards).
  • Value to mcpx. + med: a v2 plugin could answer an MCP sampling/createMessage that mcpx forwards.
  • Effort. M.
  • Risk. Low.
  • Detail. generate.text takes a plain prompt string: no messages, system prompt or tool use, so a sampling request with messages[], systemPrompt or tools must be flattened.
  • Sources. v1:packages/sdk/js/src/gen/sdk.gen.ts:615; v2:packages/core/src/plugin/host.ts:271; v2:packages/protocol/src/groups/generate.ts:8; v2:packages/core/src/generate.ts:10

PLG-34 Plugins add or change MCP servers (Location-scoped)

  • What. v2 ctx.mcp.transform(editor => editor.set/update/remove) and ctx.mcp.reload() edit the Location’s MCP server set; ctx.mcp.list() reads status. A v1 plugin can only mutate config through the config hook, once.
  • Where. opencode v2. Both versions also expose runtime MCP registration over HTTP (v1 POST /mcp; v2 PUT /api/experimental/mcp/:server, which needs the server password).
  • mcpx @ 05c78b2. Not used.
  • Value to mcpx. + med: a v2 plugin could register mcpx itself with codemode: false and protocol: "auto". v2’s own built-in opencode.mcp.codemode.exclusion plugin uses exactly this editor (CODE-53).
  • Effort. S.
  • Risk. Med: the scope is the Location, shared by all sessions and windows.
  • Detail. docs/opencode-plugin.md:346 says “registering MCP servers per session (mcp.transform)”; the MCP service is a Location node, and reconcile replaces changed servers.
  • Sources. v2:packages/plugin/src/promise/mcp.ts:15; v2:packages/core/src/plugin/host.ts:384; v2:packages/core/src/mcp/index.ts:736 “return makeLocationNode({”; v1:packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts:55; v2:packages/protocol/src/groups/mcp.ts:24; docs/opencode-plugin.md:346

PLG-35 ?codemode=false appended to remote MCP URLs

  • What. For a remote server whose config does not set codemode: false and whose URL has no codemode parameter, v2 adds codemode=false to the query, because “Servers that bundle their own Code Mode (Cloudflare and others) expose raw tools when asked”. On HTTP 400 or 404 it retries once at the configured URL.
  • Where. opencode v2 (client side). No Cloudflare document describing the parameter was found.
  • mcpx @ 05c78b2. Neither sends nor honours it: /mcp (proto.mcpPath, internal/defaults/defaults.json:77) always exposes mcpx’s meta-tools (internal/mcpserver/server.go:1-13). Whether /mcp tolerates unknown query parameters was not examined.
  • Update (2026-10-31, #1940 / task 3240). Honoured now: ServeHTTP reads codemode=false from the request URL (internal/mcpserver/codemode.go); that request’s tools/list is every upstream tool flat under <namespace>_<tool> (the registry’s Native spelling), the gateway tools are absent from it, and tools/call routes a raw name to its upstream under the upstream’s own name via Backend.Call. The backend half is mcpBackend.RawTools (internal/cli/serve.go), listing each visible namespace through the same UpstreamTools pass-through uses; a namespace that cannot be listed fails the whole list rather than serving a partial catalogue. Absence, and any value other than exactly false, change nothing. Still open: mcpx as a client does not send the parameter to Cloudflare-style servers.
  • Value to mcpx. + med: honouring it by exposing every upstream tool flat (namespaced) would let v2’s code mode orchestrate mcpx’s pooled servers directly, keeping mcpx’s per-caller isolation under v2’s catalogue. As a client, mcpx could send it to Cloudflare-style servers so its own codegen sees raw tools.
  • Effort. M — a flat-tools mode plus a query switch.
  • Risk. Med: a flat mode lists hundreds of tools, the context cost mcpx exists to avoid; it should activate only on request. A strict endpoint costs v2 a failed first connect on every start.
  • Detail. The configured URL, without the parameter, stays the OAuth identity; opencode’s SDK patch keeps the parameter out of metadata discovery. With codemode: false set (CODE-53) the parameter is never added.
  • Also recorded from the transports register. The endpoint is routed by path (internal/daemon/server.go:488), so the query string is ignored and mcpx’s meta-tools are served regardless; mcpx as a client never sends the parameter. The configured URL, without the parameter, stays the OAuth identity; v2’s SDK patch stops the parameter leaking into metadata discovery.
  • Sources. v2:packages/core/src/mcp/client.ts:214 “// Servers that bundle their own Code Mode (Cloudflare and others) expose raw tools when asked”; v2:packages/core/src/mcp/client.ts:217-218 “if (addedCodemode) url.searchParams.set("codemode", "false")”; v2:packages/core/src/mcp/client.ts:228-233; internal/defaults/defaults.json:77; v2:packages/core/src/mcp/client.ts:217; v2:packages/core/src/mcp/client.ts:218 “if (addedCodemode) url.searchParams.set("codemode", "false")”; v2:packages/core/src/mcp/client.ts:233.

PLG-36 Hooks and permission checks per child call in code mode

  • What. Each MCP call made inside opencode’s execute goes through opencode’s permission system and fires the plugin hooks. v1 fires ctx.ask({permission: key}) and tool.execute.before/after with callID = "<parent>/<n>"; v2 routes child calls through beforeExecute and executeTool, so permission.assert and execute.before/after fire per child. User rules such as “deny chrome_devtools_*” therefore apply inside code.
  • Where. opencode v1 and v2. Calls inside mcpx_exec or mcpx exec go script → daemon → server, and opencode sees one mcpx_exec or bash call. Cloudflare connectors can set requiresApproval; lootbox has nothing.
  • mcpx @ 05c78b2. The plugin only forwards opencode’s own tool outcomes to mcpx’s log (plugin/opencode/mcpx-session.ts:773-789); nothing flows from daemon calls to opencode’s permission system.
  • Value to mcpx. + high: closes a policy bypass: a tool denied in opencode is still reachable through mcpx exec.
  • Effort. L — the daemon would ask the harness per call through an elicitation-style round trip to the plugin (docs/elicitation.md §5.5).
  • Risk. High: today mcpx is a way around opencode permissions.
  • Detail. v1 builds the permission key from the MCP tool key (server_tool), so rules written for direct MCP tools apply unchanged inside code mode. mcpx’s timing hook would see v1 child calls (PLG-24).
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:141-145; v1:packages/opencode/src/tool/code-mode.ts:147; v1:packages/opencode/src/tool/code-mode.ts:227; v2:packages/core/src/tool/mcp.ts:51-63; v2:packages/core/src/tool.ts:235; plugin/opencode/mcpx-session.ts:773-789

PLG-37 Live status of each child call during a script

  • What. While execute runs, opencode publishes toolCalls: [{tool, status: running|completed|error, input}] as tool metadata (v1 ctx.metadata) or progress (v2 context.progress), and the TUI shows it.
  • Where. opencode v1 and v2. mcpx’s daemon has an event stream and /v1/exec can stream frames, but the plugin does not use them; lootbox has nothing.
  • mcpx @ 05c78b2. The plugin’s mcpx_exec makes one blocking POST and publishes nothing; its execute(args) does not even take the tool context (plugin/opencode/mcpx-session.ts:615-625; plugin/opencode/mcpx/daemon.ts:451-455).
  • Value to mcpx. + med: a user watching a two-minute browser script sees nothing until it ends.
  • Effort. S — read /v1/exec with output: "stream" and map log and emit frames to metadata.
  • Risk. Low.
  • Detail. None beyond the above.
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:216-217; v2:packages/core/src/codemode/tool.ts:85-88; plugin/opencode/mcpx-session.ts:615-625

PLG-38 Cancelling in the harness stops the script

  • What. When the user cancels, opencode aborts the running program: v1 races the interpreter against ctx.abort and passes signal: ctx.abort to each callTool; v2 interrupts the Effect fiber. The mcpx plugin’s mcpx_exec ignores the abort signal, so the daemon-side script runs until it finishes or times out.
  • Where. opencode v1 and v2. mcpx CLI: a killed CLI process kills its local script’s process group. lootbox: the CLI’s WebSocket closes but the daemon’s deno keeps running. Cloudflare: unverified.
  • mcpx @ 05c78b2. postRaw takes only an optional timeout, and exec() passes neither timeout nor signal (plugin/opencode/mcpx/daemon.ts:380-386, :452-455). The daemon would cancel on client disconnect (internal/daemon/routes_exec.go:252), but the plugin never disconnects.
  • Value to mcpx. + med: Esc on a runaway browser script should stop it.
  • Effort. S — pass the tool context’s abort signal to fetch.
  • Risk. Med: runaway scripts keep holding pooled browsers.
  • Detail. The progress-cancellation area records how an HTTP disconnect propagates to the upstream call.
  • Sources. v1:packages/opencode/src/tool/code-mode.ts:262-274; v1:packages/opencode/src/tool/code-mode.ts:155 “signal: input.ctx.abort,”; plugin/opencode/mcpx/daemon.ts:380-386; internal/daemon/routes_exec.go:252

PLG-39 Harness session and working directory reach the script

  • What. opencode v2 passes the session id to every child MCP call as _meta["ai.opencode/sessionID"]. mcpx’s plugin sets MCPX_SESSION_ID for bash, so mcpx exec from bash is session-scoped; but the plugin’s own mcpx_exec posts /v1/exec with no session and no cwd, so each call gets a fresh exec-<runID> session and the daemon’s working directory.
  • Where. opencode v2; mcpx bash path yes, plugin tool path no.
  • mcpx @ 05c78b2. exec() sends only {source, options: {output: "structured"}} (plugin/opencode/mcpx/daemon.ts:452-455); with options.session empty the service falls back to "exec-"+runID (internal/execsvc/execsvc.go:382).
  • Value to mcpx. + high: with the session, a browser persists across mcpx_exec calls in one opencode session, which AGENTS.md:87-89 promises; with cwd, repo and cwd scopes resolve to the right repository.
  • Effort. S — pass ctx.sessionID and ctx.directory as options.session and options.cwd.
  • Risk. High: repo-scoped servers resolve against the daemon’s directory, and session-scoped browsers are recreated on every call and linger (process-model area).
  • Detail. call() in the same class already accepts a session (plugin/opencode/mcpx/daemon.ts:462-467).
  • Sources. plugin/opencode/mcpx/daemon.ts:452-455; internal/execsvc/execsvc.go:382 “session := firstNonEmpty(opts.Session, ropts.Env["MCPX_SESSION"], "exec-"+runID)”; plugin/opencode/mcpx-session.ts:664-670; v2:packages/core/src/tool/mcp.ts:69 “sessionID: context.sessionID,”

PLG-40 Plugin mcpx_exec reads a field the daemon never returns

  • What. The plugin returns out.output ?? pretty(out.result ?? out). execsvc.Result has no output field (its fields are result, stdout, logs, emits, artifacts and the rest), so when stdout is not JSON the model sees the entire envelope instead of the script’s stdout.
  • Where. mcpx plugin only. opencode’s own execute returns the value, then warnings, then logs.
  • mcpx @ 05c78b2. plugin/opencode/mcpx-session.ts:619-623; Result fields at internal/execsvc/execsvc.go:201-213. The client type even declares output?: string (plugin/opencode/mcpx/daemon.ts:451).
  • Value to mcpx. + med: the model should see stdout, then error, then compact logs, not runId, emits: [], artifacts: [] and durationMs on every call.
  • Effort. S — use out.stdout (or out.error).
  • Risk. Med: every mcpx_exec wastes tokens, against the point of the feature.
  • Detail. A declared behaviour that is not delivered (build-brief rule 8), verified by reading only. The fallback used when the daemon route is absent (viaCLI(["exec", …])) returns what the CLI prints, so the two paths show the model different things for the same script.
  • Sources. plugin/opencode/mcpx-session.ts:621-623 “: (out.output ?? pretty(out.result ?? out))”; internal/execsvc/execsvc.go:201-212; v2:packages/core/src/codemode/tool.ts:293-305

PLG-41 Code execution offered as an MCP server

  • What. mcpx serves mcpx_catalog, mcpx_search, mcpx_types, mcpx_exec, mcpx_call and more over /mcp, and also runs scripts from the CLI and /v1. Cloudflare publishes code mode as an MCP server two ways: codeMcpServer() (one code tool with the upstream typed in its description) and openApiMcpServer() (search plus execute over an OpenAPI document kept in the sandbox). opencode’s code mode is reachable only from a model turn: no CLI, HTTP or MCP route. lootbox has never been an MCP server.
  • Where. mcpx and Cloudflare. The label counts the products this register tracks with labels (opencode, lootbox); Cloudflare has it too.
  • mcpx @ 05c78b2. internal/mcpserver/server.go:1-13, :327-337.
  • Value to mcpx. + med: a fixed-footprint surface any MCP host can use; Cloudflare’s “search the spec from code” idea is what mcpx_exec with in-script search()/describe() already allows.
  • Effort. S (done).
  • Risk. Low.
  • Detail. Cloudflare’s argument for server-side code mode names the CLI approach mcpx takes and rejects it for the web (“the agent needs a shell, which not every environment provides”); mcpx’s counter-argument (local-first, stateful servers, full authority) is not yet written down in its docs. In opencode v2 the only code-mode mention in protocol code is a simulation schema; POST /api/generate is text generation, not code execution.
  • Sources. internal/mcpserver/server.go:327; v2:packages/protocol/src/simulation.ts:500; v2:packages/protocol/src/groups/generate.ts:8; https://developers.cloudflare.com/agents/tools/codemode/api-reference/#codemcpserver “Wraps an existing MCP server with one code tool.”; https://developers.cloudflare.com/agents/model-context-protocol/codemode/#search-and-execute “search runs generated code against an OpenAPI document.”

Process model and statelessness

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  What each MCP revision expects of server state and connection lifetime, and how mcpx, opencode, lootbox and Cloudflare arrange processes.

Until 2025-11-25 an MCP connection is a stateful session; 2026-07-28 makes the protocol stateless by default, so a server may not use earlier requests, the connection, or the process as context, and anything that spans requests travels as an explicit identifier. For mcpx the question is sharper than for a plain server, because it is a daemon that pools upstream processes for many callers: what identifies a caller decides which browser a call reaches, and today every HTTP host of the daemon is one caller. The full list of state mcpx keeps is in ../stateless.md; this file records the items that are differences.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
PM-01One stateful session per client–server connection2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✓ — legacy caps and version held per Conn+ lowSlow
PM-02Server must not use earlier requests on a connection as context2026-07-28 has25-11 — · 26-07 MUST✓ — modern path reads _meta first+ medSlow
PM-03Cross-request state named by an explicit identifier each request2026-07-28 has25-11 — · 26-07 MUST (handles are a pattern)partial — tasks are handles; pool key is not+ highSmed
PM-04A connection or process is not a conversation2026-07-28 has25-11 — · 26-07 SHOULD/SHOULD NOTpartial — pool scope keyed on a process id+ highShigh
PM-05Server state carried through the client in requestState2026-07-28 has has better replacement25-11 — · 26-07 ✓partial — token names daemon-side call; bound to session+ medMmed
PM-06A broken stream loses the request; client re-issues2026-07-28 has mcpx has25-03..25-11 resumable · 26-07 lost✓ — modern broken-stream requests re-issued once with a new id; tools/call gated on readOnlyHint/idempotentHint (#1638)+ lowSmed
PM-07State that 2026 still permits2026-07-28 has26-07 listen, tasks, requestState, handles, legacy sessions✓ — tasks and legacy sessions allowed+ lowSlow
PM-08Request with neither modern _meta nor a prior initializespecs conflict26-07 silent for dual-era stdio; HTTP MAY assume 2025-03-26acc. — served as 2025-03-26+ lowSlow
PM-09What “supports 2026-07-28” minimally means2026-07-28 has24-11..25-11 base + lifecycle · 26-07 base + versioning + patternspartial — discover and listen shapes wrong+ highMhigh
PM-10Every HTTP MCP host shares one pool session keyopencode v2 has mcpx missingmcpx /mcp✗ — key is the daemon’s pid (#211)+ highShigh
PM-11Stateful server isolation per callermcpx has, others don't opencode v1 has opencode v2 hasmcpx per session · opencode v1 per directory · v2 per Location · lootbox shared · CF per execution id✓ — session-scoped process leases+ highSmed
PM-12/v1/exec session-scoped instances outlive the runmcpx missingmcpx only✗ — never marked ephemeral; waits for idle reap (#219)+ medSmed
PM-13MCP surface built once per daemon, never rebuiltmcpx missingmcpx only✗ — sync.Once; new adapters invisible (#219)+ medSmed
PM-14Daemon’s MCP server calls the daemon over its own socketmcpx has, others don'tmcpx only✓ — one path for stdio and HTTP− latencyMlow
PM-15/v1/protocol reports nativeElicit from the defaultmcpx missingmcpx only✗ — compiled default, not the setting (#219)+ lowSlow
PM-16opencode process topology: one process vs shared daemonopencode v1 has opencode v2 hasopencode v1 TUI + Worker per window · v2 background daemonn/a — mcpx is already a daemon+ medSmed
PM-17MCP connection scope: per directory vs per Locationopencode v1 has opencode v2 hasopencode v1 per directory per process · v2 per Location per daemonn/a — mcpx must tell sessions apart+ medSmed
PM-18When MCP servers start: awaited all vs forked eachopencode v1 has opencode v2 hasopencode v1 all at first use · v2 async per servern/a — mcpx connect time matters on v1+ lowSlow
PM-19MCP server lifetime: instance vs daemon, never idleopencode v1 has opencode v2 hasopencode v1 until dispose/exit · v2 no idle TTLn/a — mcpx serve may live for days+ lowSmed
PM-20No automatic restart when an MCP server diesopencode v1 has opencode v2 hasopencode v1 failed · v2 failedn/a — a dead mcpx serve stays dead+ medSmed
PM-21opencode database filename depends on release channelopencode v1 has opencode v2 has mcpx missingopencode.db: v1 latest/beta/prod · v2 also dev/next✗ — mcpx stats opencode reads opencode.db only (#219)+ lowSmed
PM-22lootbox connects every server at startup and pingslootbox haslootbox fork (monitor) · deployed upstream (no monitor)n/a — lazy pools, min 0+ lowSlow
PM-23lootbox re-fetches every schema on every discoverylootbox has has better replacementlootbox✓ — persisted cache invalidated by list_changed− noneSlow
PM-24lootbox multi-instance strategies and port rewritinglootbox has has better replacementlootbox fork onlyn/a — isolates callers, not daemons− misplacedSlow
PM-25Per-run script and client setuplootbox has has better replacementlootbox HTTP import + --reload · mcpx content-addressed on disk✓ — compile cache stays warm− noneSlow

PM-01 One stateful session per client–server connection

  • What. 2024-11-05 through 2025-11-25 describe MCP as a stateful session protocol: each client holds one session per server, opened by initialize, and capabilities and version hold for the whole session.
  • Where. 2024-11-05 to 2025-11-25. 2026-07-28 removes it (PM-02); a dual-era server may still run legacy sessions for clients that open with initialize.
  • mcpx @ 05c78b2. Legacy capabilities and version are stored per Conn (internal/mcpserver/conn.go:101-112); on HTTP a legacy Conn lives behind Mcp-Session-Id, on stdio it is the process.
  • Value to mcpx. + low: kept for legacy clients; nothing to change.
  • Effort. S.
  • Risk. Low.
  • Detail. The session lifetime and reaping rules (30-minute idle, reaped only when another session is issued) are recorded in the lifecycle and transports areas and in ../stateless.md.
  • Sources. 2024-11-05/architecture/index.mdx:8 “isolating concerns. Built on JSON-RPC, MCP provides a stateful session protocol focused”; 2024-11-05/architecture/index.mdx:61 “- Establishes one stateful session per server”; internal/mcpserver/conn.go:101-112

PM-02 Server must not use earlier requests on a connection as context

  • What. Servers MUST NOT rely on prior requests over the same connection to establish context (capabilities, protocol version, client identity); every request supplies it in _meta.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The modern path reads _meta before any connection state (internal/mcpserver/conn.go:133-136). The legacy HTTP question-and-answer path relies on sessions by design, for legacy peers only.
  • Value to mcpx. + med: a stateless front lets many agents share one daemon without cross-talk.
  • Effort. S (done for modern peers).
  • Risk. Low.
  • Detail. The same page adds that no state “should be inferred from previous requests, even those on the same connection or stream”; capabilities that arrive per request mean mcpx may not remember an earlier request’s elicitation declaration either.
  • Sources. 2026-07-28/basic/index.mdx:191 “- Servers MUST NOT rely on prior requests over the same connection to”; internal/mcpserver/conn.go:133-136

PM-03 Cross-request state named by an explicit identifier each request

  • What. State that spans requests (long-running tasks, application handles) MUST be referenced by an explicit identifier the client passes on each request. With sessions gone, a server that needs cross-call state mints a handle (create_basket() → basket_id) and takes it as an ordinary tool argument; there is no protocol type for handles.
  • Where. 2026-07-28 (SEP-2567).
  • mcpx @ 05c78b2. Task ids (tsk-<16 hex>) are such handles. The pool scope key is not: for /mcp hosts it comes from the serving process (PM-10), not from anything in the request.
  • Value to mcpx. + high: the 2026 way to give a host its own browser is a handle or a per-request key, which is exactly what mcpx’s pool needs as input.
  • Effort. S.
  • Risk. Med: without it the pool key stays process-derived.
  • Detail. SEP guidance for handles: opaque, at least 128 random bits when unauthenticated, validate (handle, principal) when authenticated, clear expiry errors. mcpx’s task store has no principal binding (tasks area).
  • Sources. 2026-07-28/basic/index.mdx:200 “- State that needs to span multiple requests (e.g., long-running tasks,”; 2026-07-28/changelog.mdx:12 “Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments”; seps/2567-sessionless-mcp.md:98 “That third point is not a protocol change. There is no handles/* method”

PM-04 A connection or process is not a conversation

  • What. Servers SHOULD handle requests from many tasks, threads or conversations; SHOULD NOT require a client to reuse the same connection or process for related operations; clients SHOULD NOT make a conversation the lifetime of a stdio process. The note spells it out: a server must not treat connection or process identity as a proxy for conversation or session continuity.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. As a server: mcpx serve keys pool scope on its own pid (mcp-<pid>), which is exactly process identity standing in for a conversation; over HTTP it is worse (PM-10). As a client: the session scope gives each session key its own upstream process (PM-11), tying a stdio child’s lifetime to a conversation.
  • Value to mcpx. + high: the per-request identity the spec wants is what would fix PM-10.
  • Effort. S.
  • Risk. High: sharing a stateful upstream across callers is the failure mcpx exists to prevent.
  • Detail. The client-side SHOULD NOT is aimed at modern servers that keep state in handles. A legacy stateful server (a browser MCP) has nowhere else to keep state, so per-session processes remain the only isolation for it; the tension is real but only for servers that could have used handles.
  • Sources. 2026-07-28/basic/index.mdx:194 “- Servers SHOULD be prepared to handle requests associated with multiple”; 2026-07-28/basic/index.mdx:196 “- Servers SHOULD NOT require that a client reuse the same connection or process to”; 2026-07-28/basic/index.mdx:198 “- Clients SHOULD NOT use an individual task, thread, or conversation as the”; 2026-07-28/basic/index.mdx:207 “transport, and a server must not treat connection or process identity as a”; internal/cli/serve.go:296 “return fmt.Sprintf("mcp-%d", os.Getpid())”

PM-05 Server state carried through the client in requestState

  • What. In 2026 a server that needs input returns input_required with an opaque requestState; the client echoes it on the retry. The server MUST treat it as attacker-controlled and integrity-protect it if it matters, and SHOULD bind principal, TTL and request digest. The state lives in the client, not in a session.
  • Where. 2026-07-28. The mechanics (which methods, retry ids, inputResponses) are in the MRTR area.
  • mcpx @ 05c78b2. The token is HMAC-signed with a per-process key and time-bound (proto.stateTTL 30m), but it names a call id whose live upstream call and question table stay in the daemon (askTable, result kept proto.askTTL 15m), and it is bound to the Mcp-Session-Id that 2026 removed. So a stateless 2026 client cannot complete it (elicitation area), and the daemon holds state behind the token.
  • Value to mcpx. + med: binding to a principal (or nothing, since the HMAC already authenticates) plus a method/params digest would make it work for session-less clients.
  • Effort. M.
  • Risk. Med: a live upstream call cannot be serialised into a token, so mcpx will always hold some state here; it is the server-minted-handle pattern carried in requestState, which 2026 permits (PM-07).
  • Detail. The signing key is per mcpserver.Server, so a daemon restart invalidates every outstanding token.
  • Sources. 2026-07-28/basic/patterns/mrtr.mdx:232 “1. If a client request contains a requestState field, servers MUST treat requestState as an attacker-controlled input.”; internal/mcpserver/state.go:29-51; internal/mcpserver/server.go:161-163; internal/daemon/routes_proto.go:53-65

PM-06 A broken stream loses the request; client re-issues

  • What. No redelivery: if a response stream breaks, the in-flight request is lost and the client MUST re-issue it with a new id; on stdio a crash means restart and retry.
  • Where. 2026-07-28. 2025-03-26 through 2025-11-25 had SSE resumability instead (transports area).
  • mcpx (as built, #1638 / PR #1645). Examined, and implemented for upstream calls: on a modern-era connection a request whose response stream breaks unanswered is re-issued exactly once with a new id, in Client.roundTrip (internal/mcpclient/client.go). Stream-lost identity is a data marker, never the message: the synthesized error carries code codeStreamLost (-32000) plus data {mcpxStreamLost: true}, and isStreamLost requires both — the receive loop’s own -32000 “connection closed” and any server-sent -32000 carry no marker and are never re-issued (transport death is a different mechanism, and a server’s own error is an answer). Modern era only: legacy connections keep the transport’s resume() mechanism. A call whose caller context is cancelled is never re-issued, and a second marked failure surfaces to the caller. Covered by TestModernReissueBrokenRequest (internal/mcpclient/reissue_test.go:107); the conformance gap streamable-http-client-reissue-broken-request was retired by PR #1645.
  • Value to mcpx. + low: retry-on-crash is spec-aligned for modern upstreams.
  • Effort. S.
  • Risk. Med: re-issuing a non-idempotent tool executes it twice and nothing makes the server deduplicate; the annotation gate below is the containment, and its residual is that annotations are the server’s own claim — a tool that lies about idempotentHint can still run twice.
  • Detail. The MUST applies to the client’s intent to get a result, and this entry’s earlier text left the proxy decision to mcpx. Settled by #1638, recorded as mcpx’s proxy decision: every method re-issues automatically except tools/call, which re-issues only when the tool’s list-time annotations declare readOnlyHint or idempotentHint (hints retained at ListTools by recordToolHints, consulted via toolCallReissueAllowed). An ungated tools/call surfaces the stream-lost error to the host instead of risking a double execution.
  • Sources. 2026-07-28/changelog.mdx:28 “A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID”; 2026-07-28/basic/transports/stdio.mdx:112 “Because the protocol is stateless, any in-flight requests are simply lost and”; internal/mcpclient/client.go (roundTrip, isStreamLost, recordToolHints); internal/mcpclient/reissue_test.go:107; internal/conformance/coverage_transport.go:20

PM-07 State that 2026 still permits

  • What. Stateless “by default”, not entirely: open subscriptions/listen streams (request-scoped); durable tasks in the tasks extension, polled by id; client-held requestState; application handles in tool arguments; per-request auth context; process-lifetime state on stdio (SEP: SHOULD NOT rely on it); legacy sessions of a dual-era server.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Tasks (legacy shape) and legacy HTTP sessions both exist (internal/mcpserver/server.go:475-502, 1208-1230); both are permitted. What is not permitted is minting a session for a modern peer, which server/discover does (lifecycle and transports areas).
  • Value to mcpx. + low: tells mcpx which of its stores need no change.
  • Effort. S.
  • Risk. Low.
  • Detail. The inventory in ../stateless.md classifies each mcpx store against this list.
  • Sources. 2026-07-28/basic/index.mdx:211 “Long-lived requests like”; docs/extensions/tasks/overview.mdx:56 “initial status, TTL, and suggested polling interval. The task is durably”; seps/2567-sessionless-mcp.md:237 “such servers SHOULD NOT rely on process-lifetime state and SHOULD migrate to explicit handles”; seps/2575-stateless-mcp.md:726 “Not entirely (hence ‘by default’).”

PM-08 Request with neither modern _meta nor a prior initialize

  • What. 2026-07-28 defines a modern request (with _meta) and a legacy one (after initialize), and says modern-only servers reject a missing _meta with -32602. It is silent on a dual-era server receiving a bare request with no handshake; on HTTP a dual-era server MAY treat a missing MCP-Protocol-Version as 2025-03-26.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Serves it as Oldest = 2025-03-26 (internal/mcpserver/conn.go:140-145), consistent with the HTTP MAY, on stdio as well.
  • Value to mcpx. + low: accept liberally; keep.
  • Effort. S.
  • Risk. Low: a modern client that forgot _meta silently gets legacy shapes. The stdio probe advice exists because lenient legacy servers do exactly this.
  • Detail. mcpx also ignores the legacy HTTP version header on such a request (lifecycle area).
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:278 “2025-06-18 (which did not define the MCP-Protocol-Version header) MAY”; 2026-07-28/basic/versioning.mdx:176 “- A request carrying modern per-request _meta is served statelessly”; internal/mcpserver/conn.go:140-145

PM-09 What “supports 2026-07-28” minimally means

  • What. Legacy revisions: all implementations MUST support the base protocol and lifecycle management. 2026-07-28: base protocol, versioning and the message patterns (request/response, MRTR, subscribe/notify) are MUST; everything else is optional.
  • Where. 2024-11-05..2025-11-25 vs 2026-07-28.
  • mcpx @ 05c78b2. Advertises 2026-07-28, but server/discover and subscriptions/listen shapes are wrong (lifecycle and subscriptions areas).
  • Value to mcpx. + high: the minimum is discover, per-request _meta, MRTR and listen, and nothing more.
  • Effort. M.
  • Risk. High: advertising 2026-07-28 while its mandatory patterns are malformed is “declared but not delivered”.
  • Detail. Tools, resources, prompts and the rest are optional in 2026, so a correct minimal 2026 surface is small.
  • Sources. 2024-11-05/basic/index.mdx:30 “All implementations MUST support the base protocol and lifecycle management”; 2026-07-28/basic/index.mdx:17 “All implementations MUST support the base protocol, versioning,”

PM-10 Every HTTP MCP host shares one pool session key

  • What. Calls from /mcp run with CallContext{SessionID: mcpSession()}, which is $MCPX_SESSION_ID or mcp-<pid> of the process running the MCP server. Over HTTP that process is the daemon, so every remote host lands on the same session-, pid- or cwd-scoped instance.
  • Where. mcpx. opencode v2 sends _meta["ai.opencode/sessionID"] on every tools/call, which mcpx drops (_meta area).
  • mcpx @ 05c78b2. internal/cli/serve.go:287-297; used by Call, Exec, ReadResource, GetPrompt and the asker (internal/cli/serve.go:111-112, 139, 575-576, 597-598; internal/cli/serve_ask.go:34-37).
  • Value to mcpx. + high: per-host isolation over HTTP, from ai.opencode/sessionID, a server-minted handle (PM-03), or client identity.
  • Effort. S.
  • Risk. High: two opencode sessions (or two opencode windows, PM-16) on one daemon drive one browser.
  • Detail. mcpx serve over stdio gets per-host isolation for free, since each host spawns its own process; the comment “one per process is the honest answer” is true there and false for the daemon. OPENCODE-V2.md:92-94 says nobody provides per-session isolation for opencode v2; reading the _meta key would.
  • Sources. internal/cli/serve.go:296 “return fmt.Sprintf("mcp-%d", os.Getpid())”; internal/cli/serve_ask.go:31-33 “one per process is the honest answer”; v2:packages/core/src/mcp/client.ts:278

PM-11 Stateful server isolation per caller

  • What. Who shares one instance of a stateful upstream (a browser). mcpx gives each session key its own process and pins the lease for a whole script run. opencode v1 has one connection per server per directory per opencode process, shared by every session in that window; v2 one per server per Location, shared by every session in every window attached to the daemon. lootbox holds one client per server for everyone. Cloudflare’s connections belong to one Agent, and a connector can key resources by the execution id.
  • Where. As listed; opencode v2’s only mitigation is the advisory _meta session id.
  • mcpx @ 05c78b2. Scopes global, repo, worktree, cwd, session, parent-session, pid and call (internal/config/config.go:50-71), each key its own process; sharing shared or exclusive (the package comment still names the stateful modes “pooled” and “session”, internal/pool/pool.go:3-16; key derivation internal/config/scope.go:46-64). A session scope with no session id degrades to call. Instances live until IdleTimeout with no holders, with a Min floor and a Max cap evicted LRU (internal/pool/pool.go:238-244, 697-723).
  • Value to mcpx. + high: concurrent Chrome sessions are the problem mcpx exists to solve; v2 makes the problem worse, which strengthens the case.
  • Effort. S (done; the key is the open question, PM-10).
  • Risk. Med: see PM-12 for instances that are not released.
  • Detail. The pool is invisible to MCP hosts and compatible with 2026 statelessness; only its key matters, and the key could come from request _meta or a handle (PM-03). Cloudflare’s equivalent is resource-level, not process-level: ctx.executionId passed to connector methods, plus onPassEnd/disposeExecution hooks.
  • Sources. internal/pool/pool.go:14 “"session" mode is the important one: a lease is pinned to a session key for”; v1:packages/opencode/src/cli/cmd/tui.ts:210; v2:packages/core/src/mcp/index.ts:152; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:49; https://developers.cloudflare.com/agents/tools/codemode/how-it-works/#resource-lifetimes “Connector methods receive the stable execution ID, which can key durable resource metadata across passes.”

PM-12 /v1/exec session-scoped instances outlive the run

  • What. After every run execsvc.RunWith calls Release(session), which reaches Registry.ReleaseCaller; that stops only keys where CallerOwned is true (Ephemeral, or a call: prefix). scriptEnv never sets MCPX_EPHEMERAL, so for /v1/exec runs, including the plugin’s mcpx_exec with its generated exec-<runID> session, session-scoped instances such as Chrome are not released; they wait for idle reaping.
  • Where. mcpx only. The CLI path sets MCPX_EPHEMERAL and is unaffected.
  • mcpx @ 05c78b2. internal/execsvc/execsvc.go:481-485; internal/config/scope.go:39-41; internal/execsvc/execsvc.go:573-593; the generated client forwards x-mcpx-ephemeral only from that variable (internal/codegen/emit.go:533); CLI sets it at internal/cli/commands.go:694.
  • Value to mcpx. + med: pooled browsers return promptly.
  • Effort. S.
  • Risk. Med: combined with the plugin sending no session (plugin-APIs area), a Chrome script run through the plugin gets a fresh browser every call, and each one lingers until idle timeout.
  • Detail. Inferred from code; not run.
  • Sources. internal/execsvc/execsvc.go:481-485; internal/config/scope.go:39-41; internal/codegen/emit.go:533; internal/cli/commands.go:694

PM-13 MCP surface built once per daemon, never rebuilt

  • What. lazyMCP builds the MCP server on the first /mcp request (adapters, OpenAPI documents, op tools) under a sync.Once and never rebuilds it. A config reload that adds an adapter or an API does not change tools/list, and tools.listChanged could never fire for mcpx’s own list.
  • Where. mcpx only.
  • mcpx @ 05c78b2. internal/cli/serve_ask.go:234-244; internal/cli/serve.go:332-347.
  • Value to mcpx. + med: adapters added at runtime appear without a restart.
  • Effort. S.
  • Risk. Med: users restart the daemon to see new adapter tools.
  • Detail. The build uses the first request’s context, so a cancelled first request would cache a context error for the daemon’s life (inferred from code, not triggered).
  • Sources. internal/cli/serve_ask.go:242 “l.once.Do(func() { l.srv, l.err = l.app.MCPServer(ctx) })”

PM-14 Daemon’s MCP server calls the daemon over its own socket

  • What. Every /mcp tool call goes back to the same daemon over its own /v1 socket (b.app.ensure(ctx), then c.Call), adding a hop and a second HTTP request per call. The same code serves stdio mcpx serve, where it is the right design.
  • Where. mcpx only.
  • mcpx @ 05c78b2. internal/cli/serve.go:30-117; daemon mount at internal/cli/root.go:190-195; rationale at docs/protocol.md:341-344.
  • Value to mcpx. − latency and a failure mode (a daemon that cannot reach its own socket). + one code path, and it is what makes HTTP disconnects cancel upstream calls through context propagation.
  • Effort. M to short-circuit.
  • Risk. Low.
  • Detail. Short-circuiting would have to keep the cancellation chain intact.
  • Sources. internal/cli/serve.go:104 “c, err := b.app.ensure(ctx)”; docs/protocol.md:341-344

PM-15 /v1/protocol reports nativeElicit from the default

  • What. The status endpoint says "nativeElicit": true from defaults.ProtoNative even when proto.native=false is configured, which does disable native asking.
  • Where. mcpx only.
  • mcpx @ 05c78b2. internal/daemon/routes_proto.go:530; the setting is honoured at internal/cli/serve.go:320-326.
  • Value to mcpx. + low: the endpoint’s stated reason to exist is that it “cannot disagree” with the code.
  • Effort. S.
  • Risk. Low: a declared-versus-delivered report.
  • Detail. The rest of asServer comes from the same tables the code consults (internal/daemon/routes_proto.go:495-497).
  • Sources. internal/daemon/routes_proto.go:530 “"nativeElicit": defaults.ProtoNative,”; internal/cli/serve.go:320 “if a.Settings().Bool("proto.native") {”

PM-16 opencode process topology: one process vs shared daemon

  • What. opencode v1 is one process per window: the TUI on the main thread and the server in a Worker, reached by an in-process fetch shim (http://opencode.internal); a TCP port opens only with --port, --hostname or mDNS. opencode v2’s default command resolves or starts a shared background service (“Starting background server…”) on 127.0.0.1:0xc0de (49374) for the latest/dev/beta/next channels, and every TUI connects to it over HTTP; --standalone opts out.
  • Where. v1 and v2. v2 also deletes packages/opencode: the binary is packages/cli, running @opencode/server (/api/*, typed by @opencode/protocol) over @opencode/core Effect services built per Location.
  • mcpx @ 05c78b2. Not affected directly; the plugin infers “headless” from isMainThread (plugin/opencode/mcpx-session.ts:161), which is a v1 assumption.
  • Value to mcpx. + med: v2 is architecturally close to mcpx (daemon plus thin clients) and offers a typed HTTP API mcpx could call.
  • Effort. S.
  • Risk. Med: under v2 one server, one plugin instance per Location and one MCP connection per server per Location serve every window (PM-11).
  • Detail. v2’s Mode is "default" | "service" | "stdio", and the service may be registered with the OS. opencode serve/run in v1 put the server on the main thread.
  • Sources. v1:packages/opencode/src/cli/cmd/tui.ts:210 “const worker = new Worker(file, {”; v1:packages/opencode/src/cli/cmd/tui.ts:246; v2:packages/cli/src/commands/handlers/default.ts:31; v2:packages/cli/src/services/service-config.ts:35 “return 0xc0de”; v2:packages/cli/src/services/server-connection.ts:41; v2:packages/cli/package.json:7

PM-17 MCP connection scope: per directory vs per Location

  • What. v1 keeps MCP clients in InstanceState, a cache keyed by directory; v2 keeps a ServerEntry {client?, tools?, prompts?} map in a Location-scoped service (directory plus workspace id). All sessions of that directory or Location share each server’s single connection.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. n/a; opencode will never give mcpx more than one connection per server entry per place, so mcpx must tell sessions apart itself (PM-10).
  • Value to mcpx. + med.
  • Effort. S.
  • Risk. Med.
  • Detail. v2 canonicalises the Location key before use. v2 spawns stdio servers in the Location’s Environment, which may be a remote workspace (v2:packages/core/src/mcp/client.ts:198); v1 spawns them on the host.
  • Sources. v1:packages/opencode/src/effect/instance-state.ts:49; v1:packages/opencode/src/mcp/index.ts:492; v2:packages/core/src/mcp/index.ts:67; v2:packages/core/src/mcp/index.ts:152; v2:packages/core/src/location-service-map.ts:21

PM-18 When MCP servers start: awaited all vs forked each

  • What. v1 connects every configured server concurrently when MCP state is first touched, so the first MCP access in a directory waits for the slowest server (up to its timeout). v2 forks each startServer, publishes ToolsChanged as each lands, and only callTool/prompt/readResource wait on that server’s startup latch.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. n/a.
  • Value to mcpx. + low: mcpx serve start time delays every v1 session’s first request.
  • Effort. S.
  • Risk. Low.
  • Detail. v1’s MCP state is not part of instance bootstrap; it materialises lazily on the first InstanceState.get.
  • Sources. v1:packages/opencode/src/mcp/index.ts:505; v1:packages/opencode/src/mcp/index.ts:528; v2:packages/core/src/mcp/index.ts:499; v2:packages/core/src/mcp/index.ts:507

PM-19 MCP server lifetime: instance vs daemon, never idle

  • What. v1 closes clients (and kills descendants) when the directory instance is disposed or the process exits. v2 retains Location service graphs with idleTimeToLive: Duration.infinity, so MCP servers live as long as the daemon.
  • Where. opencode v1 and v2.
  • mcpx @ 05c78b2. n/a; an mcpx serve child of the v2 daemon can live for days.
  • Value to mcpx. + low: mcpx must not assume a short-lived parent.
  • Effort. S.
  • Risk. Med: resource leaks in mcpx serve surface under v2 first.
  • Detail. v2 invalidates a Location only on boot failure or explicit invalidation. Teardown differs too: v1 SIGTERMs descendants; v2 signals the process group, SIGKILL after 2 s (lifecycle area).
  • Sources. v1:packages/opencode/src/mcp/index.ts:531; v2:packages/core/src/location-services.ts:52 “{ idleTimeToLive: Duration.infinity },”

PM-20 No automatic restart when an MCP server dies

  • What. On transport close both versions mark the server failed: "Connection closed", drop its tools and publish ToolsChanged; neither respawns it. Recovery is a manual connect.
  • Where. opencode v1 and v2. v2 does reconnect when a credential for that server’s integration changes.
  • mcpx @ 05c78b2. n/a; if mcpx serve exits, opencode loses mcpx until the user reconnects.
  • Value to mcpx. + med: the stdio side should survive daemon restarts and upgrades.
  • Effort. S.
  • Risk. Med: a daemon upgrade that kills mcpx serve silently removes mcpx from every open opencode session.
  • Detail. 2026-07-28 expects a client to restart a crashed stdio server and retry (PM-06); neither opencode version does.
  • Sources. v1:packages/opencode/src/mcp/index.ts:448; v2:packages/core/src/mcp/index.ts:358; v2:packages/core/src/mcp/index.ts:543

PM-21 opencode database filename depends on release channel

  • What. v1 uses opencode.db only for the latest, beta and prod channels, else opencode-<channel>.db; v2 uses opencode.db for latest, dev, beta, next and prod. Both honour OPENCODE_DB and OPENCODE_DISABLE_CHANNEL_DB.
  • Where. opencode v1 and v2; they disagree on whether next gets its own file.
  • mcpx @ 05c78b2. mcpx stats opencode looks only for opencode.db (internal/opencode/opencode.go:37, :46); this machine has both opencode.db and opencode-next.db.
  • Value to mcpx. + low: enumerate opencode*.db or read the channel.
  • Effort. S.
  • Risk. Med: stats silently read the wrong opencode’s database.
  • Detail. A v1 next build writes opencode-next.db; a v2 next build writes opencode.db.
  • Sources. v1:packages/core/src/database/database.ts:49; v2:packages/cli/src/database-path.ts:7; internal/opencode/opencode.go:37

PM-22 lootbox connects every server at startup and pings

  • What. lootbox connects to every configured server before its HTTP listener opens, keeps one client per server for the daemon’s lifetime, and (in the fork) runs a health monitor that pings each server, reconnects with exponential backoff and gives up after a set number of failures.
  • Where. lootbox. The deployed daemon on :9420 is not the fork: it is upstream jx-codes/lootbox@587a5a1 plus four patches (~/.config/nix/flake.nix, lines 386–398), and has neither the monitor nor a deep /health (the live /health returned {"status":"ok"}). The fork-only set also includes the multi-instance strategies (PM-24) and the configurable timeout and permissions.
  • mcpx @ 05c78b2. Lazy pools (pool.min 0, max 4, idle 5 m, internal/defaults/defaults.json:2-10); Client.Ping exists (internal/mcpclient/client.go:726-727).
  • Value to mcpx. + low: a ping could spot a wedged stateful server before a script blocks on it.
  • Effort. S.
  • Risk. Low: pinging an idle lazy server keeps it alive and defeats idleTimeout.
  • Detail. Startup waits for every server (await clientManager.initializeClients(...) before Deno.serve); ASSESSMENT.md:100 measured 43 s before the port opened. opencode v2 starts asynchronously (PM-18); Cloudflare restores an Agent’s connections after hibernation.
  • Sources. .lootbox/README.md:385-388 “Lootbox automatically monitors MCP server health with periodic ping() probes.”; .lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:221-222; .lootbox/src/lib/rpc/websocket_server.ts:190

PM-23 lootbox re-fetches every schema on every discovery

  • What. GET /namespaces, /rpc-namespaces and /types/:ns all call getSchemas(), which runs tools/list and resources/list against every connected server in turn; the cache accessor getAllSchemas() is stubbed to return [].
  • Where. lootbox (fork and deployed; the path is identical in both).
  • mcpx @ 05c78b2. A persisted schema cache (internal/daemon/registry.go:87-100) invalidated by list_changed (internal/mcpclient/client.go:495-500).
  • Value to mcpx. − already solved.
  • Effort. S.
  • Risk. Low.
  • Detail. lootbox calls listTools() with no cursor, so only the first page is ever read.
  • Sources. .lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:380-396; .lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:137-140 “// return Array.from(this.cache.values()); return [];”; .lootbox/src/lib/external-mcps/mcp_schema_fetcher.ts:69

PM-24 lootbox multi-instance strategies and port rewriting

  • What. When two lootbox daemons configure the same server, the warn/fail/auto-port/per-session strategies coordinate them through a file registry; auto-port finds a port in the server’s arguments (for example --remote-debugging-port) and rewrites it to a free one.
  • Where. lootbox fork only.
  • mcpx @ 05c78b2. n/a: mcpx isolates callers within one daemon (PM-11).
  • Value to mcpx. − ASSESSMENT.md puts it plainly: “the thing that needs isolating is the caller, not the process”; there is only ever one lootbox instance, a launchd singleton.
  • Effort. S.
  • Risk. Low.
  • Detail. per-session does what warn does without the warning: it registers a session and spawns one shared client, although the README says “Spawn independent server per session”.
  • Sources. .lootbox/src/lib/rpc/managers/mcp_integration_manager.ts:200-216; ASSESSMENT.md:95-96 “The abstraction is at the wrong level: the thing that needs isolating is the caller, not the process.”; .lootbox/README.md:417 “| per-session | Spawn independent server per session |”

PM-25 Per-run script and client setup

  • What. lootbox writes a temp file whose first line imports the client over HTTP from the daemon itself, then runs deno run --reload=http://localhost:<port>/client.ts, re-fetching and re-compiling the client every time. mcpx writes the client to a directory named after its content hash only when it changes, and names inline scripts after their own hash, so the runtime’s compile cache stays warm.
  • Where. lootbox; mcpx. opencode runs in-process; Cloudflare uses a new isolate (CODE-04).
  • mcpx @ 05c78b2. internal/runner/runner.go:4-9 (the rationale, naming lootbox), :245-251, :276-291, :455-475.
  • Value to mcpx. − already solved; ASSESSMENT.md measured lootbox exec 'console.log(1)' at 0.74–10.3 s.
  • Effort. S.
  • Risk. Low.
  • Detail. lootbox’s client URL already carries ?v=<version>, so --reload is redundant. A lootbox script runs in the daemon’s working directory, not the caller’s (code-mode area).
  • Sources. .lootbox/src/lib/execute_llm_script.ts:18; .lootbox/src/lib/execute_llm_script.ts:33; .lootbox/src/lib/execute_llm_script.ts:13; internal/runner/runner.go:4-9

Progress and cancellation

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  notifications/progress and progress tokens, timeouts, notifications/cancelled and 2026 transport-specific cancellation, in the spec, mcpx, opencode and lootbox

Progress and cancellation let a long request report on itself and be stopped: progress gained a message in 2025-03-26 and became server-only in 2026-07-28, and 2026 made cancellation transport-specific, so that closing an HTTP response stream is the cancel and a client’s notifications/cancelled is defined only on stdio. mcpx never asks upstreams for progress and never relays it to hosts, so long calls look hung and opencode v1’s 60-second timer cannot be kept alive (PC-04, PC-05); an inbound notifications/cancelled is recorded and acted on by nothing, while an HTTP disconnect does cancel the upstream call, for legacy hosts too (PC-11, PC-12). The request-scoping rule shared with log messages and a server ending a listen stream with notifications/cancelled are in notifications.md; progress on tasks is in tasks.md.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
PC-01notifications/progress fields; message added2025-03-26 has24-11 token, progress, total · 25-03+ + messagepartial — parsed from upstreams onto the event bus only+ lowSlow
PC-02progressToken rules: string or integer, unique among active requests2024-11-05 hasall five; chosen by “sender” (24-11) → “client” (26-07)n/a — never issues or forwards a token+ lowSlow
PC-03Progress direction: either side → server only2026-07-28 removes24-11..25-11 both ways · 26-07 server → clientpartial — host progress answered with a -32601 error+ lowSlow
PC-04Client requests progress with _meta.progressTokenopencode v1 has opencode v2 has mcpx missing 2024-11-05 hasopencode v1/v2: token = request id; mcpx ✗; lootbox ✗✓ — a call whose host sent a token carries one of mcpx’s own (#212)+ medSmed
PC-05Proxy relays progress between host and upstreammcpx missingevery revision (server → client)✓ — relayed under the host’s token (#212; docs/protocol.md §4.4)+ highMmed
PC-06Timeouts, reset on progress, hard maximum2025-03-26 has opencode v1 has24-11 "appropriate" · 25-03+ SHOULD time out, MAY reset, SHOULD cappartial — fixed 120 s call timeout; no progress reset+ medSmed
PC-07notifications/cancelled requestId: required → optional → required2025-11-25 has 2026-07-28 has24-11..25-06 required · 25-11 optional · 26-07 required✓ — always sends it; tolerates its absence+ lowSlow
PC-08Cancellation reasonmcpx missingoptional string in every revision✗ — client always says "timeout" (#203)+ lowSlow
PC-09initialize MUST NOT be cancelled2024-11-05 has 2026-07-28 removes mcpx missing24-11..25-11 ✓ · 26-07 — (no initialize)✗ — a timed-out initialize gets a cancel (#203)+ lowSlow
PC-10Who may cancel: either side → client only (+ listen)2026-07-28 has24-11..25-11 either side · 26-07 client; server only for listen✓ — cancels only its own relayed legacy requests+ lowSlow
PC-11Receiver stops work on notifications/cancelledimpl deferred mcpx missingevery revision (stdio only in 26-07)partial — recorded; no upstream call is interrupted (#77)+ medMmed
PC-12HTTP: closing the stream is cancellation (2026), not (legacy)2026-07-28 has specs conflict opencode v2 has25-03..25-11 SHOULD NOT · 26-07 MUSTpartial — disconnect cancels for every era; interruptible calls survive+ medMmed
PC-13Client notifications/cancelled: stdio only in 20262026-07-28 has specs conflict mcpx missing24-11..25-11 any transport · 26-07 stdio MUST, HTTP undefinedpartial — aborts the POST and also sends the notification (#203)+ lowSlow
PC-14A caller’s abort or timeout cancels the upstream callopencode v1 has opencode v2 hasopencode ✓; mcpx ✓; lootbox ✗✓ — context end sends notifications/cancelled upstream+ medSlow

PC-01 notifications/progress fields; message added

  • What. progressToken, progress (MUST increase with each notification) and optional total, in every revision; 2025-03-26 adds an optional human-readable message that SHOULD be relevant. progress and total MAY be floating point.
  • Where. Base fields in all five; message from 2025-03-26.
  • mcpx @ 05c78b2. As a client it parses all four fields, message included (internal/mcpclient/client.go:436-442), and publishes them as a Progress event on the bus (internal/daemon/hooks.go:34-36). As a server it emits none (PC-05).
  • Value to mcpx. + low: the parsing is done; what is missing is the relay.
  • Effort. S.
  • Risk. low.
  • Detail. The 2026 changelog also makes progress request-scoped (notifications.md, SUB-07). A 2026 embedded sampling request has no _meta, so no token and no progress for it (sampling.md).
  • Sources. schema/2024-11-05/schema.ts:271 “notifications/progress”; schema/2025-03-26/schema.ts:316 “message?: string;”; 2025-03-26/changelog.mdx:24 “Added”; 2024-11-05/basic/utilities/progress.mdx:52 “values MAY be floating point.”; internal/mcpclient/client.go:436-442; internal/daemon/hooks.go:34-36.

PC-02 progressToken rules: string or integer, unique among active requests

  • What. A requester opts in by putting progressToken in the request’s _meta. Tokens MUST be a string or integer and MUST be unique across all active requests; notifications MUST reference only tokens from an active request, and MUST stop after completion. The receiver MAY send none at all.
  • Where. All five. 2024-11-05 says the token is “chosen by the sender”; 2026 says “chosen by the client”, since only clients request progress there (PC-03).
  • mcpx @ 05c78b2. Issues no tokens upstream and forwards none from hosts (PC-04, PC-05).
  • Value to mcpx. + low: the rules a relay must follow when it starts relaying.
  • Effort. S.
  • Risk. low.
  • Detail. A host’s token is unique only among that host’s active requests. A relay that multiplexes many hosts onto one upstream connection therefore has to mint its own upstream tokens and map them back, rather than pass hosts’ tokens through. Unspecified in 2026: whether an MRTR retry may reuse the original request’s token (mrtr.md). opencode uses the JSON-RPC request id as the token (PC-04).
  • Sources. 2024-11-05/basic/utilities/progress.mdx:15 “Progress tokens MUST be a string or integer value”; 2024-11-05/basic/utilities/progress.mdx:16-17 “across all active requests.”; 2026-07-28/basic/patterns/progress.mdx:17 “Progress tokens can be chosen by the client using any means”; 2024-11-05/basic/utilities/progress.mdx:56 “Progress notifications MUST only reference tokens that:”; 2024-11-05/basic/utilities/progress.mdx:88 “Progress notifications MUST stop after completion”.

PC-03 Progress direction: either side → server only

  • What. Legacy: either party may request and send progress, so a client can report progress on a server-initiated request (a long elicitation, say). 2026: only the server sends progress, only for tokens from an active client request, only on that request’s stream.
  • Where. Both ways in 2024-11-05..2025-11-25; server to client in 2026-07-28.
  • mcpx @ 05c78b2. mcpx never asks a host for progress on the questions it relays, and a host that sends notifications/progress anyway gets an id-less -32601 error back (wire S5; the hygiene bug is notifications.md, SUB-18).
  • Value to mcpx. + low: nothing to build; stop replying to the notification.
  • Effort. S.
  • Risk. low.
  • Detail. Client-to-server progress disappears in 2026 because server-initiated requests do; legacy ClientNotification includes ProgressNotification and 2026’s does not.
  • Sources. 2024-11-05/basic/utilities/progress.mdx:7 “Either side can send progress notifications to”; 2026-07-28/basic/patterns/progress.mdx:8 “The server MAY send progress notifications”; wire S5.

PC-04 Client requests progress with _meta.progressToken

  • What. Whether a client asks its server for progress at all. Without a token, a conforming server sends none.
  • Where. opencode v1 and v2 pass a no-op onprogress to every callTool, which makes the SDK attach _meta.progressToken equal to the numeric request id; the values are otherwise ignored (not shown to the user or the model). v1 code-mode child calls do the same. lootbox sends no token. mcpx sends none.
  • mcpx @ 05c78b2. CallTool sends {name, arguments} only (internal/mcpclient/client.go:849); progressToken appears only in the struct it parses (internal/mcpclient/client.go:438).
  • Value to mcpx. + med: upstream tools that report progress (browsers, crawls) could extend mcpx’s call deadline (PC-06) and feed /v1/exec streams and plugin metadata.
  • Effort. S — attach a token per upstream call; the notification path already exists.
  • Risk. med: without it, a long upstream call is killed at the fixed call timeout even while it is making progress.
  • Detail. opencode’s v1 comment says why: “The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.”
  • Also recorded from the meta register. Every revision. opencode v1 and v2 pass a no-op progress handler, which makes their SDKs set _meta.progressToken to the numeric request id on every tools/call; the values are not shown to anyone. v1 resets its 60 s call timeout on progress; v2 does not. tools/call reads only name and arguments (internal/mcpserver/server.go:658-665), and the ask path deletes _meta before dispatch (internal/mcpserver/ask.go:100), so the host’s token reaches nothing. The client never sets a token upstream. Forwarding progress itself is in the progress register.
  • Sources. v1:packages/opencode/src/mcp/catalog.ts:64; v1:packages/opencode/src/tool/code-mode.ts:154-158; v2:packages/core/src/mcp/client.ts:281; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/shared/protocol.js (L649, progressToken: messageId); internal/mcpclient/client.go:849; internal/mcpclient/client.go:438.; schema/2024-11-05/schema.ts:25; 2026-07-28/basic/index.mdx:352; internal/mcpserver/ask.go:100; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/shared/protocol.js L649 “progressToken: messageId”.

PC-05 Proxy relays progress between host and upstream

  • What. A proxy in the middle of a call can pass the host’s progress request upstream and the upstream’s progress back to the host, on the host request’s own stream.
  • Where. Progress exists in every revision; mcpx relays none of it.
  • mcpx @ 05c78b2. The host’s _meta (and with it progressToken) is stripped before the call (forAsk, internal/mcpserver/ask.go:95-108); upstream calls carry no token (PC-04); progress that does arrive goes to the event bus only (internal/daemon/hooks.go:34-36); internal/mcpserver has no notifications/progress sender.
  • Value to mcpx. + high: progress for long mcpx_call and mcpx_exec runs in hosts, and, for opencode v1, the only way to keep a call alive past the SDK’s 60-second default, because v1 resets its timer on progress (PC-06).
  • Effort. M — token mapping per call (PC-02) and request-scoped delivery on HTTP (an SSE response) and stdio.
  • Risk. med: long calls look hung, and v1 kills them at 60 s.
  • Detail. In 2026 progress MUST go only on the originating request’s stream, never on a listen stream (notifications.md, SUB-07). mcpx already opens an SSE response for legacy elicitation, which is the same mechanism.
  • Sources. internal/mcpserver/ask.go:100 “delete(m, "_meta")”; internal/daemon/hooks.go:34-36; 2026-07-28/basic/patterns/progress.mdx:14; v1:packages/opencode/src/mcp/catalog.ts:61.

PC-06 Timeouts, reset on progress, hard maximum

  • What. 2024-11-05 says only to implement appropriate timeouts. From 2025-03-26: implementations SHOULD time out every request and SHOULD cancel on timeout, SDKs SHOULD allow per-request timeouts, implementations MAY reset the clock on progress but SHOULD always enforce a maximum. 2026 moves the text from the lifecycle page to the cancellation page and phrases “cancel” per transport.
  • Where. 2025-03-26 onward. opencode v1: connect and list 30 s; tools/call uses the per-server timeout or experimental.mcp_timeout, else the SDK’s 60 s, with resetTimeoutOnProgress: true and no maxTotalTimeout. opencode v2: startup 30 s, catalog 30 s, execution 12 h, with no reset on progress. lootbox: a Promise.race per call that progress cannot reset.
  • mcpx @ 05c78b2. A fixed pool.callTimeout of 120 s (internal/defaults/defaults.json:6), configurable but not extended by progress, since mcpx requests none (PC-04). On timeout the client cancels upstream (PC-14).
  • Value to mcpx. + med: reset-on-progress up to a hard wall is the spec’s recommended shape and what long tools need.
  • Effort. S once PC-04 exists.
  • Risk. med: a server that sends progress forever never times out under a reset-only policy, which is why the SHOULD asks for a maximum. opencode v1 has no maximum.
  • Detail. opencode v2’s comment says “Requesting progress keeps long calls alive under the SDK’s timeout; execution is the hard wall”, but it never sets resetTimeoutOnProgress, so progress has no effect and the 12 h value is the timeout. (The code-mode findings say v2 resets on progress too; the v2 source says otherwise.) opencode v1’s config text says the default is 5 s while its code uses 30 s, and docs/opencode-plugin.md:178 says “one value, 30 s”, which is only the connect and list timeout.
  • Sources. 2025-03-26/basic/lifecycle.mdx:199 “Implementations SHOULD establish timeouts for all sent requests”; 2025-03-26/basic/lifecycle.mdx:208 “Implementations MAY choose to reset the timeout clock when receiving a”; 2026-07-28/basic/patterns/cancellation.mdx:63 “always enforce a maximum timeout, regardless of progress notifications”; v1:packages/opencode/src/mcp/catalog.ts:61 “resetTimeoutOnProgress: true,”; v1:packages/opencode/src/mcp/index.ts:38 “const DEFAULT_TIMEOUT = 30_000”; v2:packages/core/src/mcp/client.ts:34 “const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000”; v2:packages/core/src/mcp/client.ts:280 “Requesting progress keeps long calls alive under the SDK’s timeout”; v1:packages/core/src/v1/config/mcp.ts:21; .lootbox/src/lib/rpc/execute_mcp.ts:199-208; internal/defaults/defaults.json:6 “"callTimeout": "120s",”.

PC-07 notifications/cancelled requestId: required → optional → required

  • What. requestId is required through 2025-06-18. 2025-11-25 makes it optional so the notification’s shape can coexist with tasks (it MUST be present for non-task requests and MUST NOT be used for tasks, which use tasks/cancel). 2026 makes it required again, tasks having left the core.
  • Where. Required 2024-11-05..2025-06-18 and 2026-07-28; optional in 2025-11-25.
  • mcpx @ 05c78b2. Always sends it, as client and as server (internal/mcpclient/client.go:714; internal/mcpserver/conn.go:266). Inbound, a cancel without requestId is accepted and recorded under the key <nil> (internal/mcpserver/server.go:1521-1538), which does no harm because nothing reads the record (PC-11).
  • Value to mcpx. + low: accept a missing requestId from 2025-11-25 peers without error, which mcpx does.
  • Effort. S.
  • Risk. low: a strict parser would reject 2025-11-25 cancels without the id.
  • Detail. Neither changelog mentions either change.
  • Sources. schema/2024-11-05/schema.ts:132 “requestId: RequestId;”; schema/2025-11-25/schema.ts:223 “requestId?: RequestId;”; schema/2026-07-28/schema.ts:626 “requestId: RequestId;”; internal/mcpserver/server.go:1533 “c.cancelled[fmt.Sprint(p.RequestID)] = p.Reason”.

PC-08 Cancellation reason

  • What. An optional free-text reason on notifications/cancelled; both parties SHOULD log it.
  • Where. Every revision.
  • mcpx @ 05c78b2. The client says "timeout" whatever ended the context: a host’s cancellation, a disconnect, or a real timeout (internal/mcpclient/client.go:714). A legacy question mcpx relayed and then gave up on is cancelled with "timed out" (internal/mcpserver/conn.go:266), which is accurate.
  • Value to mcpx. + low: honest upstream logs.
  • Effort. S — take the reason from the context’s cause.
  • Risk. low: misleading upstream logs.
  • Detail. None beyond the above.
  • Sources. schema/2024-11-05/schema.ts:137 “reason?: string;”; 2026-07-28/basic/patterns/cancellation.mdx:108 “Both parties SHOULD log cancellation reasons for debugging”; internal/mcpclient/client.go:714 “"reason": "timeout"”.

PC-09 initialize MUST NOT be cancelled

  • What. Every legacy revision forbids a client to cancel its initialize request, in prose and schema.
  • Where. 2024-11-05..2025-11-25; 2026 has no initialize.
  • mcpx @ 05c78b2. The client’s generic path sends notifications/cancelled for any request whose context ends (internal/mcpclient/client.go:708-716), initialize included.
  • Value to mcpx. + low: skip the cancel for initialize.
  • Effort. S.
  • Risk. low: a strict server may treat it as a protocol error.
  • Detail. Found by reading the code path; not driven on the wire.
  • Sources. 2024-11-05/basic/utilities/cancellation.mdx:34 “request MUST NOT be cancelled by clients”; schema/2025-11-25/schema.ts:238 “A client MUST NOT attempt to cancel its”; internal/mcpclient/client.go:708-716.

PC-10 Who may cancel: either side → client only (+ listen)

  • What. Legacy: either party may cancel a request it issued, in the same direction. 2026: the client cancels its own requests; a server may send notifications/cancelled only to end a subscriptions/listen stream (see notifications.md, SUB-11), because it has no requests of its own to cancel.
  • Where. Either side in 2024-11-05..2025-11-25; client (plus listen teardown) in 2026-07-28.
  • mcpx @ 05c78b2. As a legacy server it cancels its own relayed questions when it stops waiting for them (internal/mcpserver/conn.go:258-266). For 2026 hosts it sends no requests, so it has nothing to cancel.
  • Value to mcpx. + low: correct.
  • Effort. S.
  • Risk. low.
  • Detail. The 2026 page still opens with “A client SHOULD send a cancellation notification”, generically, although on HTTP the client cancels by closing (PC-13).
  • Sources. 2024-11-05/basic/utilities/cancellation.mdx:7 “Either side can send a cancellation notification to”; 2026-07-28/basic/patterns/cancellation.mdx:8 “A client SHOULD send a cancellation notification”; internal/mcpserver/conn.go:265-266.

PC-11 Receiver stops work on notifications/cancelled

  • What. A receiver SHOULD stop processing the cancelled request, free its resources and not send a response; it MAY ignore the cancel if the request is unknown, finished, or cannot be cancelled.
  • Where. Every revision; in 2026 the notification exists only on stdio (PC-13).
  • mcpx @ 05c78b2. The cancel is recorded in a per-connection map (internal/mcpserver/server.go:642-647, :1521-1538) and OnCancel is a field nothing sets (internal/mcpserver/server.go:134-135), so no upstream call is interrupted. The doc says so and defers it (docs/protocol.md:88, :372-374).
  • Value to mcpx. + med: stdio hosts could stop long scripts and calls.
  • Effort. M — keep a context.CancelFunc per request id on each connection; on stdio this first needs requests handled off the read loop (a transports-register item).
  • Risk. med: work runs on after the host gave up; the recorded map on the default stdio connection grows for the process lifetime.
  • Detail. Recording is conformant under the MAY (the request “cannot be cancelled” today), but useless. Over HTTP the disconnect path already cancels (PC-12), which makes stdio the gap.
  • Sources. 2025-06-18/basic/utilities/cancellation.mdx:36 “Receivers of cancellation notifications SHOULD:”; 2026-07-28/basic/patterns/cancellation.mdx:73 “Servers receiving cancellation notifications SHOULD:”; docs/protocol.md:88 “recorded; mcpx cannot yet interrupt an upstream call mid-flight”; docs/protocol.md:372 “Inbound cancellation does not reach an upstream call.”; internal/mcpserver/server.go:134-135.

PC-12 HTTP: closing the stream is cancellation (2026), not (legacy)

  • What. Legacy Streamable HTTP: a disconnect SHOULD NOT be read as cancellation; to cancel, the client SHOULD send notifications/cancelled. 2026: closing the request’s SSE response stream MUST be treated as cancellation of that request.
  • Where. 2025-03-26..2025-11-25 versus 2026-07-28. opencode v2 on a modern HTTP connection aborts the HTTP request.
  • mcpx @ 05c78b2. When an HTTP host closes its POST, r.Context() ends (internal/mcpserver/server.go:1176-1177), which cancels the backend’s /v1/call to the daemon (internal/cli/client.go:197), the daemon handler’s reg.Call (internal/daemon/server.go:696), and the pool call, whose client then cancels upstream (internal/mcpclient/client.go:709-716). This matches 2026, is not era-gated, and is not described in the docs. For an interruptible call the upstream work runs as a daemon task and is deliberately not cancelled (internal/mcpserver/ask.go:157-162; mrtr.md, MRTR-20).
  • Value to mcpx. + med: already works; document and test it, and gate it by era.
  • Effort. M — era gating plus a test that drives a real disconnect.
  • Risk. med: treating a legacy host’s dropped stream as a cancel kills work a resuming legacy client expected to keep; not treating a 2026 host’s as one wastes upstream work.
  • Detail. Inferred from the context chain; no disconnect was driven on the wire, and buffering in the unix-socket HTTP client was not traced. On 2026 HTTP a plain JSON (non-SSE) response has no stream to close, so dropping the connection is the only signal.
  • Sources. 2025-03-26/basic/transports.mdx:122 “Disconnection SHOULD NOT be interpreted as the client cancelling its request.”; 2026-07-28/basic/transports/streamable-http.mdx:235 “Closing the SSE response stream MUST be treated by the server as”; 2026-07-28/basic/patterns/cancellation.mdx:39 “Closing the SSE response stream is the cancellation signal.”; internal/mcpserver/server.go:1176-1177; internal/daemon/server.go:696; internal/mcpclient/client.go:709-716; v2:packages/core/src/mcp/client.ts:273.

PC-13 Client notifications/cancelled: stdio only in 2026

  • What. 2026 stdio: the client MUST send notifications/cancelled (there is no per-request stream to close). 2026 HTTP defines no client-to-server notifications at all; closing the stream is the cancel and no notification is expected. Yet the cancellation page opens with a generic “A client SHOULD send a cancellation notification”.
  • Where. Any transport in 2024-11-05..2025-11-25; stdio only in 2026-07-28.
  • mcpx @ 05c78b2. On a 2026 HTTP upstream the client does both: the request’s own POST is aborted by the same context (internal/mcpclient/http.go:115), which is the correct signal, and it also sends notifications/cancelled (internal/mcpclient/client.go:713-715), which 2026 HTTP does not define.
  • Value to mcpx. + low: drop the notification for 2026 HTTP upstreams.
  • Effort. S.
  • Risk. low: a strict 2026 server may reject the notification POST.
  • Detail. Read the SHOULD as applying to stdio. Header requirements for notification POSTs are “not defined by this revision”, so a dual-era server should not reject a header-less notification; mcpx’s own server answers one with 202 (wire W26).
  • Sources. 2026-07-28/basic/patterns/cancellation.mdx:42 “There is no per-request stream to close. The client MUST send a”; 2026-07-28/basic/transports/streamable-http.mdx:95 “This revision of the core protocol defines no client-to-server”; 2026-07-28/basic/transports/streamable-http.mdx:98-104; internal/mcpclient/client.go:713-715; internal/mcpclient/http.go:115; wire W26.

PC-14 A caller’s abort or timeout cancels the upstream call

  • What. When the thing that started a tool call gives up (the user presses Esc, a script times out or dies), the in-flight upstream request should be cancelled rather than left running.
  • Where. opencode v1 and v2 pass the call’s AbortSignal to callTool, and the SDK sends notifications/cancelled {requestId, reason} on abort or timeout (v2 on modern HTTP aborts the request instead). mcpx cancels through the request context. lootbox’s per-call timeout is a Promise.race that abandons the promise without cancelling it, and no signal is passed, so a killed script’s calls keep running in the daemon until the SDK’s own 60 s timeout cancels them.
  • mcpx @ 05c78b2. /v1/call passes r.Context() to reg.Call (internal/daemon/server.go:696); when it ends, the client sends notifications/cancelled upstream (internal/mcpclient/client.go:709-716). A script that times out has its process group killed (internal/runner/runner.go:424-438), which drops its socket requests and so cancels their upstream calls the same way.
  • Value to mcpx. + med: already done; it keeps pool slots and upstream work from leaking.
  • Effort. S — done.
  • Risk. low. The opencode plugin’s mcpx_exec passes no abort signal to the daemon, so a user’s cancel in opencode does not reach an mcpx script (a code-mode register item).
  • Detail. The reason is always "timeout" (PC-08), and inbound cancellation of mcpx itself is PC-11.
  • Sources. v1:packages/opencode/src/mcp/catalog.ts:62 “signal: options.abortSignal,”; v2:packages/core/src/mcp/client.ts:273; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/shared/protocol.js (L677); internal/daemon/server.go:696; internal/mcpclient/client.go:709-716; .lootbox/src/lib/rpc/execute_mcp.ts:199-208; .lootbox/src/lib/rpc/execute_mcp.ts:61.

Prompts

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Prompt, PromptArgument and PromptMessage per revision, content allowed in prompt messages, and how mcpx and opencode pass prompts on

Prompts are server-authored message templates a user picks — opencode turns each into a slash command — and the shape has grown only by optional fields (title, icons, _meta) and by what a message may carry (audio in 2025-03-26, resource_link in 2025-06-18). What matters most at 05c78b2 is that mcpx collapses every prompts/get into one user text message, and sends the 2025-06-18 title to 2025-03-26 hosts because lists are never revision-filtered. Content-block types are in the content-types register, prompts.listChanged in the capabilities register, cache hints in the pagination-caching register, and prompts/get failures reported as -32602 are ERR-04 in the resources register.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
PRM-01Prompt.title2025-06-18 has mcpx missing24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — kept, but also sent to 2025-03-26 hosts (#207)+ lowSlow
PRM-02PromptArgument.title: in the schema, not in 2025-06-18 prose2025-06-18 has specs conflict mcpx missing25-06 ✓ (schema) · 25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ lowSlow
PRM-03Prompt.icons (not on arguments)2025-11-25 has mcpx missing25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ lowSlow
PRM-04_meta on Prompt (not on arguments)2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ medSmed
PRM-05audio allowed in PromptMessage.content2025-03-26 has24-11 T I R · 25-03 +audio · later same✗ — dropped by flattening (PRM-07)+ lowSlow
PRM-06resource_link allowed in prompt messages; prose lags until 20262025-06-18 has specs conflict25-06 ✓ (schema) · 25-11 ✓ (schema) · 26-07 ✓ (schema + prose)✗ — dropped by flattening (PRM-07)+ lowSlow
PRM-07mcpx flattens prompts/get into one user text message2024-11-05 has mcpx missingevery revision allows many messages, roles, content types✗ — roles, order and non-text content lost (#206)+ medSmed
PRM-08opencode turns MCP prompts into slash commands (text only)opencode v1 has opencode v2 hasopencode v1 ✓ ($1..$n) · v2 ✓ (server:prompt)✓ — mcpx prompts appear as commands+ lowSlow

PRM-01 Prompt.title

  • What. A human display name for a prompt, separate from name, via BaseMetadata.
  • Where. 2025-06-18 onward; documented in 2025-06-18 prose.
  • mcpx @ 05c78b2. The client keeps an upstream title (internal/mcpclient/client.go:786), and mcpx’s PromptRef emits it (internal/mcpserver/server.go:94) to every host. downgrade() touches only content, messages, structuredContent and resultType (internal/mcpserver/revisions.go:132-170), so a 2025-03-26 host receives a field its revision does not define — a send-conservatively conflict.
  • Value to mcpx. + low; the fix is the list filtering the tools register (TOOL-25) also needs.
  • Effort. S.
  • Risk. Low: unknown fields are usually ignored. The same gap applies to any future icons or _meta.
  • Detail. The 2025-03-26 Prompt is name, description, arguments only.
  • Sources. schema/2025-06-18/schema.ts:701 “export interface Prompt extends BaseMetadata {”; 2025-06-18/server/prompts.mdx:176 “- title: Optional human-readable name of the prompt for display purposes.”; schema/2025-03-26/schema.ts:599-612; internal/mcpserver/server.go:94 “Title string ; internal/mcpserver/revisions.go:132-170`

PRM-02 PromptArgument.title

  • What. Each prompt argument may carry a display title.
  • Where. In the 2025-06-18 schema onward (PromptArgument extends BaseMetadata); the 2025-06-18 prose documents Prompt.title but not the argument’s.
  • mcpx @ 05c78b2. Neither the client’s PromptArgument (internal/mcpclient/client.go:792-796) nor mcpx’s PromptArg (internal/mcpserver/server.go:100-104) has a title, so it is dropped.
  • Value to mcpx. + low: argument labels in a host’s prompt form.
  • Effort. S.
  • Risk. Low.
  • Detail. PromptReference in completion requests gains title the same way (completion register).
  • Sources. schema/2025-06-18/schema.ts:722 “export interface PromptArgument extends BaseMetadata {”; internal/mcpclient/client.go:792-796; internal/mcpserver/server.go:100-104

PRM-03 Prompt.icons

  • What. Prompt extends BaseMetadata, Icons. PromptArgument gets no icons.
  • Where. 2025-11-25 and 2026.
  • mcpx @ 05c78b2. The client Prompt struct has name, title, description, arguments only (internal/mcpclient/client.go:784-789); icons are dropped.
  • Value to mcpx. + display.
  • Effort. S.
  • Risk. Low.
  • Detail. Same Icon shape as tools and resources.
  • Sources. schema/2025-11-25/schema.ts:984 “export interface Prompt extends BaseMetadata, Icons {”; 2025-11-25/server/prompts.mdx:185 “- icons: Optional array of icons for display in user interfaces”; internal/mcpclient/client.go:784-789

PRM-04 _meta on Prompt

  • What. Arbitrary metadata on a prompt definition. PromptArgument has none.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. Dropped on parse (internal/mcpclient/client.go:784-789).
  • Value to mcpx. + extension data must survive the proxy.
  • Effort. S.
  • Risk. Extension metadata silently lost.
  • Detail. See the tools register for _meta on Tool.
  • Sources. schema/2025-06-18/schema.ts:714 “_meta?: { [key: string]: unknown };”; internal/mcpclient/client.go:784-789

PRM-05 audio in prompt messages

  • What. 2024-11-05 prompt messages carry text, image or an embedded resource; 2025-03-26 adds audio. PromptMessage.content is always one block, never an array.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. downgradeMessages rewrites each message’s single block per revision (internal/mcpserver/revisions.go:236-258), and audio’s floor is 2025-03-26, which is mcpx’s own floor, so no rewrite is ever needed. In practice audio never reaches a host at all: prompts/get keeps only text (PRM-07).
  • Value to mcpx. + low.
  • Effort. S (with PRM-07).
  • Risk. Low.
  • Detail. mcpx does not serve 2024-11-05, the one revision where audio in a prompt would be invalid.
  • Sources. schema/2024-11-05/schema.ts:607 “content: TextContent | ImageContent | EmbeddedResource;”; schema/2025-03-26/schema.ts:645 “content: TextContent | ImageContent | AudioContent | EmbeddedResource;”; internal/mcpserver/revisions.go:236-258
  • What. From 2025-06-18 PromptMessage.content is a ContentBlock, so a prompt may carry a resource_link (a URI to fetch) instead of embedding contents.
  • Where. In the schema from 2025-06-18. The 2025-06-18 and 2025-11-25 prompts prose documents only text, image, audio and embedded resources, and the 2025-06-18 changelog advertises resource links only “in tool call results”; a “Resource Links” section first appears in 2026 prose.
  • mcpx @ 05c78b2. downgradeMessages would turn a resource_link into an embedded resource for pre-2025-06-18 hosts (internal/mcpserver/revisions.go:248-253), but PRM-07 means no link ever reaches it.
  • Value to mcpx. + low.
  • Effort. S (with PRM-07).
  • Risk. Low.
  • Detail. A host implemented from the 2025-06-18 prose alone may not expect links in prompts.
  • Sources. schema/2025-06-18/schema.ts:750 “content: ContentBlock;”; 2026-07-28/server/prompts.mdx:274 “#### Resource Links”; 2025-06-18/changelog.mdx:27 “Add support for resource links in”; internal/mcpserver/revisions.go:248-253

PRM-07 mcpx flattens prompts/get

  • What. A GetPromptResult is an optional description plus messages[], each with a role (user or assistant) and one content block — enough for few-shot prompts that alternate roles and carry images or resources.
  • Where. Every revision.
  • mcpx @ 05c78b2. The upstream result is rendered to one string by renderPrompt (internal/cli/serve.go:640-669): the description is folded into the text, non-user roles become [role] prefixes (internal/cli/serve.go:662-664), non-text content is dropped, and mcpx answers with a single {role:"user", content:{type:"text"}} message (internal/mcpserver/server.go:759-764).
  • Value to mcpx. + few-shot prompts keep working through mcpx; images and resources survive.
  • Effort. S — pass messages through and let downgradeMessages do its job.
  • Risk. If not fixed: prompt semantics change silently — an assistant turn becomes user text.
  • Detail. downgradeMessages exists (internal/mcpserver/revisions.go:236-258) but only ever sees one text block. opencode also keeps only the text of prompt messages (PRM-08), so for opencode hosts the loss is mostly the roles.
  • Also recorded from the content types register. Multi-message, multi-role prompts exist in every revision. opencode v1 and v2 use only the text of prompt messages anyway (slash commands). renderPrompt (internal/cli/serve.go:640-669), used by prompts/get (internal/mcpserver/server.go:759-764) and the ask path (internal/mcpserver/ask.go:271-275). An embedded text resource in a prompt message is dropped too: renderPrompt reads only content.text, and a resource’s text sits at content.resource.text.
  • Sources. internal/cli/serve.go:662-664 “fmt.Fprintf(&b, “; internal/cli/serve.go:640-669; internal/mcpserver/server.go:759-764; internal/mcpserver/revisions.go:236-258; internal/mcpserver/server.go:760-763; schema/2024-11-05/schema.ts:607 “content: TextContent | ImageContent | EmbeddedResource;”

PRM-08 opencode prompts as slash commands

  • What. opencode lists prompts from servers that declare prompts and exposes each as a slash command, using only the text content of the resulting messages.
  • Where. opencode v1: command key sanitize(server):sanitize(prompt), positional arguments mapped to $1..$n, getPrompt fetched lazily when the template is read. opencode v2: server:prompt, parsed arguments, refreshed on prompts/list_changed.
  • mcpx @ 05c78b2. mcpx re-exposes upstream prompts as <ns>_<name> (wire W19), so under an opencode server entry called mcpx a prompt appears as mcpx:<ns>_<name>.
  • Value to mcpx. + mcpx-proxied prompts are usable in opencode with no extra work; − image and resource content in a prompt never reaches opencode either way.
  • Effort. S.
  • Risk. Low.
  • Detail. v1 fetches prompts fresh on each command listing (no cache), so it ignores prompts/list_changed without going stale for long.
  • Sources. v1:packages/opencode/src/command/index.ts:105; v1:packages/opencode/src/command/index.ts:124; v1:packages/opencode/src/mcp/catalog.ts:108; v2:packages/core/src/plugin/command.ts:55; v2:packages/core/src/plugin/command.ts:105

Resources

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Resource and ResourceTemplate fields, resources/read contents, subscriptions and resource errors per revision, and how mcpx, opencode and lootbox expose resources

Resources are read-only data a server publishes by URI, listed directly or through URI templates; mcpx re-exposes every upstream’s resources under mcpx://<ns>/… and publishes script artifacts as a template of its own. What mcpx sends back at 05c78b2 is lossy — templates carry uri instead of the required uriTemplate, binary and multi-part contents collapse into one text item, optional fields are dropped on parse — and resources/subscribe is accepted and declared on stdio but never reaches an upstream. List-change and subscribe capabilities are in the capabilities register, cache hints in the pagination-caching register, resource_link and embedded resources in the content-types register; ERR-06 and ERR-04 overlap the errors register.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
RES-01Resource.title / ResourceTemplate.title2025-06-18 has mcpx missing24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ lowSlow
RES-02Resource.size: in the 2024-11-05 schema, in prose from 2025-03-262024-11-05 has specs conflict mcpx missingall five ✓ (schema) · prose from 25-03 · templates never✗ — dropped on parse (#207)+ medSlow
RES-03resource annotations (audience, priority)2024-11-05 has mcpx missingall five ✓ (mixin until 25-03, same wire)✗ — dropped on parse (#207)+ medSlow
RES-04Annotations.lastModified2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — dropped with annotations (#207)+ lowSlow
RES-05Resource.icons / ResourceTemplate.icons2025-11-25 has mcpx missing25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ lowSlow
RES-06_meta on Resource, ResourceTemplate, read contents2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓ · 26-07 ✓✗ — dropped on parse and on read (#207)+ medSmed
RES-07ResourceTemplate.uriTemplate (required)2024-11-05 has mcpx missingall five ✓✗ — broken: mcpx sends uri (wire W18) (#207)+ highShigh
RES-08upstream URIs re-exposed as mcpx://<ns>/<uri>mcpx has, others don'tmcpx onlypartial — disambiguates; leading / trimmed so URIs can collide+ medSlow
RES-09text vs base64 blob contents, unchanged since 2024-11-052024-11-05 has mcpx missingall five ✓ (+ _meta 25-06)✗ — always text; binary as base64 text or a placeholder (#206)+ medSmed
RES-10contents[] may hold several items2024-11-05 has mcpx missingall five ✓✗ — merged into one text item under the requested URI (#206)+ lowSlow
RES-11a missing resource MUST NOT read as empty contents2026-07-28 has26-07 ✓ only✓ — failures are errors, never contents: []+ lowSlow
RES-12resources/subscribe removed in 20262024-11-05 has 2026-07-28 removes has better replacement24-11..25-11 ✓ · 26-07 rem (listen filter) · opencode never sendsacc. — accepted from every era+ medSlow
RES-13resources/unsubscribe removed in 20262024-11-05 has 2026-07-28 removes has better replacement24-11..25-11 ✓ · 26-07 remacc. — accepted from every era+ lowSlow
RES-14notifications/resources/updated listen-only and tagged in 20262024-11-05 has 2026-07-28 has mcpx missing24-11..25-11 after subscribe · 26-07 listen stream + subscriptionIdpartial — sent untagged (#208)+ medSmed
RES-15resources/updated carries the upstream URI, not mcpx://2024-11-05 has mcpx missingall five (the uri param)✗ — the host cannot correlate the update (#208)+ medSmed
RES-16how resources reach the model in each hostopencode v1 has opencode v2 hasopencode v1 3 model tools · v2 TUI @ list only · mcpx tool + script API✓ — mcpx_resource_read and readResource()+ medSmed

RES-01 Resource.title / ResourceTemplate.title

  • What. A human display name, separate from the programmatic name (which stays required), via BaseMetadata.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. The client’s Resource struct has only uri, uriTemplate, name, description, mimeType (internal/mcpclient/client.go:137-143), so title is gone before mcpx re-exposes anything; mcpx’s own ResourceRef has no title either (internal/mcpserver/server.go:84-89).
  • Value to mcpx. + display in hosts and in mcpx_resources.
  • Effort. S — but re-exposing it needs revision filtering, since lists are not downgraded (tools register).
  • Risk. Display data lost.
  • Detail. Same loss for Prompt-side titles is in the prompts register.
  • Sources. schema/2025-06-18/schema.ts:526 “export interface Resource extends BaseMetadata {”; 2025-06-18/server/resources.mdx:279 “- title: Optional human-readable name of the resource for display purposes.”; internal/mcpclient/client.go:137-143; internal/mcpserver/server.go:84-89

RES-02 Resource.size

  • What. Optional raw size in bytes (before base64), so a client can estimate context use before reading.
  • Where. In the schema of all five revisions; the 2024-11-05 prose field list omits it, 2025-03-26 prose adds it. ResourceTemplate never has size.
  • mcpx @ 05c78b2. Dropped on parse (internal/mcpclient/client.go:137-143) and absent from ResourceRef (internal/mcpserver/server.go:84-89).
  • Value to mcpx. + a size before a read is exactly what an agent needs to decide whether to pull a resource into context — mcpx’s own goal.
  • Effort. S.
  • Risk. Low.
  • Detail. mcpx’s artifacts already know their size (it goes into the resource_link description), so mcpx could emit size for its own resources too.
  • Sources. schema/2024-11-05/schema.ts:449 “size?: number;”; 2024-11-05/server/resources.mdx:276 “- mimeType: Optional MIME type”; 2025-03-26/server/resources.mdx:277 “- size: Optional size in bytes”; internal/mcpclient/client.go:137-143

RES-03 resource annotations

  • What. annotations: {audience?, priority?} on resources and templates. 2024-11-05 gets it through the Annotated mixin; 2025-03-26 names the type Annotations and adds an explicit field — same JSON.
  • Where. All five. The rename is not in the 2025-03-26 changelog, and the prose “Annotations” section first appears in 2025-06-18.
  • mcpx @ 05c78b2. Dropped on parse (internal/mcpclient/client.go:137-143).
  • Value to mcpx. + audience: ["user"] is the upstream saying “keep this out of the model’s context”; priority (0..1) orders what to read first.
  • Effort. S.
  • Risk. Low.
  • Detail. The same Annotations type sits on content blocks (content-types register).
  • Sources. schema/2024-11-05/schema.ts:417 “export interface Resource extends Annotated {”; schema/2025-03-26/schema.ts:475 “annotations?: Annotations;”; internal/mcpclient/client.go:137-143

RES-04 Annotations.lastModified

  • What. Optional ISO 8601 last-modified timestamp in Annotations, on resources, templates and content blocks.
  • Where. 2025-06-18 onward. The 2025-06-18 changelog does not list it.
  • mcpx @ 05c78b2. Dropped along with annotations (RES-03).
  • Value to mcpx. + a freshness hint for cached reads.
  • Effort. S.
  • Risk. Low.
  • Detail. None beyond RES-03.
  • Sources. schema/2025-06-18/schema.ts:1127 “lastModified?: string;”; 2025-06-18/server/resources.mdx:314 “lastModified: An ISO 8601 formatted timestamp”

RES-05 Resource.icons / ResourceTemplate.icons

  • What. Resource and ResourceTemplate extend Icons.
  • Where. 2025-11-25 and 2026.
  • mcpx @ 05c78b2. Dropped on parse (internal/mcpclient/client.go:137-143).
  • Value to mcpx. + display.
  • Effort. S.
  • Risk. Low.
  • Detail. Same Icon shape as tools (tools register).
  • Sources. schema/2025-11-25/schema.ts:802 “export interface Resource extends BaseMetadata, Icons {”; schema/2025-11-25/schema.ts:845 “export interface ResourceTemplate extends BaseMetadata, Icons {”

RES-06 _meta on resources and read contents

  • What. Arbitrary metadata on each Resource, ResourceTemplate and each item of resources/read contents.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. Dropped on parse for listings (internal/mcpclient/client.go:137-143) and when reads are flattened to text (internal/cli/serve.go:608-637).
  • Value to mcpx. + extensions (MCP Apps UI resources among them) carry their data here.
  • Effort. S.
  • Risk. Extension metadata silently lost.
  • Detail. See the tools register for _meta on Tool.
  • Sources. schema/2025-06-18/schema.ts:561 “_meta?: { [key: string]: unknown };”; schema/2025-06-18/schema.ts:620; internal/cli/serve.go:608-637

RES-07 ResourceTemplate.uriTemplate

  • What. A template carries its RFC 6570 URI template in the required field uriTemplate; there is no uri on a template.
  • Where. All five.
  • mcpx @ 05c78b2. Broken. resources/templates/list returns []ResourceRef, whose only URI field is json:"uri" (internal/mcpserver/server.go:85, internal/mcpserver/server.go:574-593, internal/cli/serve.go:684-698). Wire W18: {"uri":"mcpx://artifacts/{id}","name":"mcpx artifact",…}. To a schema-validating host these are malformed; a lax one finds no uriTemplate to expand.
  • Value to mcpx. + mcpx’s artifact template and every upstream template become usable by hosts.
  • Effort. S — a separate template type with uriTemplate.
  • Risk. If not fixed: every host ignores or rejects mcpx’s templates.
  • Detail. The client side reads uriTemplate correctly (internal/mcpclient/client.go:139). lootbox reads uriTemplate off resources/list entries, where it never appears (CODE-46).
  • Sources. schema/2025-11-25/schema.ts:851 “uriTemplate: string;”; internal/mcpserver/server.go:85 “URI string ; internal/cli/serve.go:684-698; internal/mcpclient/client.go:139`

RES-08 mcpx://<ns>/<uri> namespacing

  • What. mcpx re-exposes each upstream resource as mcpx://<namespace>/<original-uri>, appends “ (ns)“ to its description, and keeps only name, description and mimeType.
  • Where. mcpx only; it is how a proxy keeps two servers’ identical URIs apart (tools register, TOOL-02, for the same rule on names).
  • mcpx @ 05c78b2. internal/cli/serve.go:486-495. A leading / is trimmed from the upstream URI (internal/cli/serve.go:490), so an upstream URI /abs and abs map to the same mcpx://ns/abs.
  • Value to mcpx. + disambiguation is necessary; − the optional fields dropped with it (RES-01..RES-06).
  • Effort. S.
  • Risk. Low; the collision needs an upstream that publishes bare paths.
  • Detail. The daemon’s /v1/resources and the mcpx_resources tool use the same listing. Reads strip the prefix back off before calling the upstream.
  • Sources. internal/cli/serve.go:490 “URI: "mcpx://" + r.Namespace + "/" + strings.TrimPrefix(r.URI, "/"),”; internal/cli/serve.go:486-495

RES-09 text versus blob contents

  • What. resources/read returns contents: (TextResourceContents | BlobResourceContents)[]: uri, optional mimeType, and either text or base64 blob. Only _meta was added (2025-06-18).
  • Where. All five, otherwise unchanged.
  • mcpx @ 05c78b2. Every read answers one TextResourceContents (internal/mcpserver/server.go:720-722). An mcpx artifact with a binary type becomes base64 in text under e.g. image/png (internal/cli/exec.go:548-552). An upstream blob becomes the placeholder (N bytes of mime, base64) (internal/cli/serve.go:608-637), where N is the length of the base64 string, not the byte count.
  • Value to mcpx. + hosts that can render images get them; − blobs cost context, but resources/read is the explicit “I want the bytes” call.
  • Effort. S — emit blob for binary.
  • Risk. A host that trusts mimeType tries to decode text as an image; an upstream binary resource is unreadable through mcpx’s MCP surface.
  • Detail. The first item’s mimeType becomes the whole result’s (internal/cli/serve.go:621-624); an empty type becomes text/plain.
  • Also recorded from the content types register. BlobResourceContents in every revision. Result shape at internal/mcpserver/server.go:720-722; artifacts at internal/cli/exec.go:548-552; upstream flattening in renderResource (internal/cli/serve.go:608-637). Several upstream contents[] items, possibly with different URIs and types, are joined with newlines into one item under the requested URI, and the first item’s mimeType becomes the whole result’s (internal/cli/serve.go:621-624, :636); per-part URIs (a directory listing, say) are lost.
  • Sources. schema/2024-11-05/schema.ts:359 “contents: (TextResourceContents | BlobResourceContents)[];”; schema/2026-07-28/schema.ts:1230; internal/cli/exec.go:552 “return base64.StdEncoding.EncodeToString(body), mime, nil”; internal/mcpserver/server.go:720-722; schema/2024-11-05/schema.ts:506; schema/2025-03-26/schema.ts:537-548; internal/cli/serve.go:636 “return strings.Join(parts, "\n"), mime, nil”

RES-10 several contents items merged into one

  • What. A read may return several items, with different URIs and types (a directory listing, a multi-part document).
  • Where. All five.
  • mcpx @ 05c78b2. The upstream contents[] is joined with newlines into one text item under the requested URI (internal/cli/serve.go:619-636); the ask path uses the same renderer (internal/cli/serve_ask.go:196-201).
  • Value to mcpx. + low.
  • Effort. S — pass the array through with rewritten URIs.
  • Risk. Per-part URIs and types are lost.
  • Detail. Blob parts become placeholders inside the joined text (RES-09).
  • Sources. internal/cli/serve.go:636 “return strings.Join(parts, “; internal/cli/serve_ask.go:196-201

RES-11 no empty contents for a missing resource

  • What. 2026 states outright that “not found” must be an error, never contents: []; an empty array stays legal for a resource that exists but is empty.
  • Where. 2026; not stated in earlier revisions.
  • mcpx @ 05c78b2. Read failures are errors (internal/mcpserver/server.go:713-716), and a success always carries exactly one item (internal/mcpserver/server.go:720-722).
  • Value to mcpx. + low: already conforms.
  • Effort. None.
  • Risk. Low. The one-item shape has its own ambiguity: an upstream that returns contents: [] for an existing empty resource reaches the host as one item with empty text.
  • Detail. None beyond the above.
  • Sources. 2026-07-28/server/resources.mdx:410 “Servers MUST NOT return an empty contents array for a non-existent resource.”; internal/mcpserver/server.go:720-722

RES-12 resources/subscribe

  • What. A legacy per-URI subscribe RPC. 2026 removes it; a client lists the URIs in resourceSubscriptions on subscriptions/listen instead.
  • Where. 2024-11-05 through 2025-11-25. Neither opencode version ever sends it.
  • mcpx @ 05c78b2. Accepted from any era (internal/mcpserver/server.go:595-620) — accept liberally. The per-URI set is kept on the connection and mapped onto restartListen with only the URI list (internal/mcpserver/server.go:619), which replaces any list-changed filter the connection had open.
  • Value to mcpx. + accepting from every era is right.
  • Effort. S.
  • Risk. A host mixing subscriptions/listen for list changes and resources/subscribe on stdio loses its list-changed filter on the first subscribe. As a client, calling it against a pure-2026 upstream would get -32601 (mcpx’s client makes it a no-op for modern upstreams).
  • Detail. Over HTTP the call answers {} and nothing can ever be pushed (capabilities register, CAP-17).
  • Sources. schema/2025-11-25/schema.ts:751 “method: "resources/subscribe";”; 2026-07-28/changelog.mdx:18 “Replace the HTTP GET endpoint and resources/subscribe/resources/unsubscribe with subscriptions/listen”; internal/mcpserver/server.go:619 “s.restartListen(c, ListenFilter{ResourceSubscriptions: uris})”

RES-13 resources/unsubscribe

  • What. The legacy counterpart of RES-12. 2026 has no per-URI unsubscribe: the client cancels the listen stream and opens a new one with a different filter.
  • Where. 2024-11-05 through 2025-11-25.
  • mcpx @ 05c78b2. Accepted from any era (internal/mcpserver/server.go:595); on stdio it answered {} on the wire (stdio run 2).
  • Value to mcpx. + fine as is.
  • Effort. Done.
  • Risk. None.
  • Detail. Unsubscribing the last URI restarts the listener with an empty filter.
  • Sources. schema/2025-11-25/schema.ts:769 “method: "resources/unsubscribe";”; 2026-07-28/basic/patterns/subscriptions.mdx:9 “the client cancels it. It replaces the former resources/subscribe RPC and the HTTP GET”; internal/mcpserver/server.go:595

RES-14 notifications/resources/updated in 2026

  • What. The notification and its uri param are unchanged, but in 2026 it is sent only on a subscriptions/listen stream for URIs in resourceSubscriptions, and every listen-stream notification MUST carry _meta["io.modelcontextprotocol/subscriptionId"].
  • Where. All five; delivery changes in 2026.
  • mcpx @ 05c78b2. Emitted with params {uri} and no _meta (internal/events/events.go:260-261), to legacy and modern listeners alike.
  • Value to mcpx. + 2026 stdio hosts can attribute updates to a subscription.
  • Effort. S — tag on listen streams only; legacy subscribers must not get the key.
  • Risk. 2026 hosts multiplexing several subscriptions cannot correlate untagged notifications.
  • Detail. In every revision the uri “might be a sub-resource of the one that the client actually subscribed to”. The tagging rule applies to all listen-stream notifications (notifications register).
  • Sources. schema/2026-07-28/schema.ts:1421 “This is only sent for resources the client opted in to via the resourceSubscriptions field”; schema/2024-11-05/schema.ts:403 “method: "notifications/resources/updated";”; internal/events/events.go:261

RES-15 resources/updated carries the upstream URI

  • What. A host subscribes to the URI it was given; the update has to name that URI.
  • Where. Every revision.
  • mcpx @ 05c78b2. The listener strips mcpx://<ns>/ from the host’s URIs to match upstream events (internal/cli/serve.go:747-758) but the outgoing notification carries the upstream URI (internal/events/events.go:261), which the host never saw. The namespace is discarded in the strip, so the same URI from two servers matches both.
  • Value to mcpx. + updates become usable.
  • Effort. S — re-prefix on the way out and keep the namespace in the filter.
  • Risk. If not fixed: every update is uncorrelatable for the host.
  • Detail. Only matters once SUB-15 makes updates arrive at all.
  • Sources. internal/events/events.go:261 “return "notifications/resources/updated", map[string]any{"uri": e.URI}, true”; internal/cli/serve.go:747-758

RES-16 how resources reach the model

  • What. Every revision calls resources “application-driven”: the host decides how they reach the model. Hosts differ sharply.
  • Where. opencode v1 adds three model-facing tools — list_mcp_resources, list_mcp_resource_templates, read_mcp_resource — to every request once any connected server declares resources, even in code mode; reads go through a read permission on mcp:<server>:<uri>. opencode v2 lists resources and templates only for the TUI @ autocomplete and GET /api/mcp/resource; Mcp.readResource exists with no caller. lootbox generates functions (CODE-46).
  • mcpx @ 05c78b2. Resources are reachable through the mcpx_resources, mcpx_resource_templates and mcpx_resource_read op tools (wire W6) and from scripts through one readResource(uri) export (internal/execsvc/execsvc.go:610-611).
  • Value to mcpx. + opencode v1 pays three schemas per request just because mcpx declares resources (capabilities register, CAP-16); v2 never reads them, so mcpx’s own tools are the only route for a v2 model.
  • Effort. S.
  • Risk. Context cost in v1.
  • Detail. v1’s three tools duplicate mcpx’s own resource tools when mcpx is the server.
  • Sources. 2026-07-28/server/resources.mdx:24 “Resources in MCP are designed to be application-driven, with host applications”; v1:packages/opencode/src/session/tools.ts:27; v1:packages/opencode/src/session/tools.ts:136; v1:packages/opencode/src/session/tools.ts:388; v2:packages/tui/src/component/prompt/autocomplete.tsx:413; v2:packages/core/src/mcp/index.ts:713; internal/execsvc/execsvc.go:610-611

Roots

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-11-01T16:22:00-06:00
increment:    1
status:       standard
tags:         area:compare
description:  roots/list, the roots capability and its list_changed notification across revisions, and what mcpx, opencode and lootbox answer

Roots let a server ask the client which filesystem locations it should work in. The request is in every revision; 2026-07-28 drops the change notification and the listChanged flag, carries roots/list only as an embedded input request, and deprecates the feature in favour of tool parameters or configuration. mcpx declares roots and answers from configured roots, and relays a roots request to the originating host when that host declared roots; otherwise it uses its configured fallback (ROOT-01/04). opencode declares roots in both versions and answers with one root, its directory.

Status column: ROOT-01/03/04 were refreshed at 2a93f3f64; other rows retain the mcpx @ 05c78b2 snapshot. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
ROOT-01roots/list and what a client answers2024-11-05 has 2026-07-28 deprecates opencode v1 has opencode v2 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep. (MRTR); opencode one root; lootbox ✗✓ — declares roots and answers configured roots, or the calling host’s roots when relayed (modern.go:342-359; hooks.go:373-409)+ medSlow
ROOT-02notifications/roots/list_changed removed2024-11-05 has 2026-07-28 removes24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✗ — answers the notification with a -32601 error+ lowSlow
ROOT-03Roots deprecated; ListRootsRequest embedded in InputRequiredResult2026-07-28 deprecates has better replacement26-07 only✓ — embedded roots/list answered like legacy− deprecatedSlow
ROOT-04Server asks its host for roots2026-07-28 deprecatesspec: any server may; mcpx relays when the request has a calling client✓ — forwards roots/list to the declared host’s roots handler (hooks.go:373-409; relay_caps_test.go:78-134)+ medM — donelow
ROOT-05Per-request roots _meta keyspecs conflictSEP-2575 lists it; 2026-07-28 defines no such keyn/a — neither sent nor read− mootSlow

ROOT-01 roots/list and what a client answers

  • What. A server asks the client for Root{uri, name?} entries; uri must be a file:// URI. A client that supports it declares the roots capability. _meta on Root arrives in 2025-06-18.
  • Where. All five revisions; in 2026-07-28 only as an MRTR input request, and deprecated. opencode v1 and v2 both declare roots and answer one root, the opencode directory (v1 the instance directory, v2 the Location directory). lootbox declares no capabilities, so its SDK would answer -32601.
  • mcpx @ 2a93f3f64. It declares roots (internal/mcpclient/modern.go:102-108) and answers from configured roots (internal/mcpclient/modern.go:342-359). When a server asks during a call whose originating client declared roots, the daemon relays the request to that client; otherwise mcpx falls back to its configured roots (internal/daemon/hooks.go:373-409).
  • Value to mcpx. + med: filesystem servers can use configured roots or the calling client’s workspace.
  • Effort. S — configured-root fallback and call-scoped host relay are implemented.
  • Risk. low — an empty configured-root list is legal; relayed roots stay scoped to the host call that requested them.
  • Detail. The natural roots vary per scope key (cwd, repo, worktree), so a pooled instance shared across keys has no single right answer; per-key instances do. opencode’s root is the opencode directory, not the git worktree. 2026 prose spells out that roots are “informational guidance rather than an access-control mechanism”.
  • Sources. schema/2024-11-05/schema.ts:1029 “roots/list”; schema/2025-06-18/schema.ts:1439 “_meta?: { [key: string]: unknown };”; schema/2026-07-28/schema.ts:2760 “This must start with”; internal/mcpclient/modern.go:102-108,342-359; internal/daemon/hooks.go:373-409; v1:packages/opencode/src/mcp/index.ts:78 “Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }),”; v2:packages/core/src/mcp/client.ts:155; v2:packages/core/src/mcp/client.ts:145 “Some legacy servers refuse to run without one”; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153; 2026-07-28/client/roots.mdx:22 “are informational guidance rather than an access-control mechanism.”.

ROOT-02 notifications/roots/list_changed removed

  • What. The client-to-server “my roots changed” notification. Legacy clients that declared listChanged: true MUST send it on change. 2026 removes it; the only client notification left is notifications/cancelled.
  • Where. 2024-11-05..2025-11-25.
  • mcpx @ 05c78b2. As a client it declares listChanged: false, so it never has to send one. As a server it has no handler, and the default branch answers with a -32601 error (internal/mcpserver/server.go:766). Wire S5: {"jsonrpc":"2.0","error":{"code":-32601,"message":"no method notifications/roots/list_changed"}}, a response to a notification, which MUST NOT get one.
  • Value to mcpx. + low: stop replying; host-root relay is implemented in ROOT-04.
  • Effort. S — ignore notifications (the general fix is in notifications.md).
  • Risk. low: strict hosts log a protocol error on every roots change.
  • Detail. opencode declares roots without listChanged, so it never sends this notification to mcpx.
  • Sources. schema/2025-11-25/schema.ts:2146 “notifications/roots/list_changed”; 2025-11-25/client/roots.mdx:78 “When roots change, clients that support”; schema/2026-07-28/schema.ts:3166 “export type ClientNotification = CancelledNotification;”; internal/mcpserver/server.go:766; wire S5.

ROOT-03 Roots deprecated; ListRootsRequest embedded in InputRequiredResult

  • What. 2026 deprecates Roots (SEP-2577): migrate to tool parameters, resource URIs or server configuration. ListRootsRequest becomes a bare {method, params?: {_meta?}} inside inputRequests, and ListRootsResult no longer extends Result.
  • Where. 2026-07-28.
  • mcpx @ 2a93f3f64. The client answers an embedded roots/list with the same answer() as a legacy request (internal/mcpclient/modern.go:342-359), so a 2026 upstream gets configured roots or the originating host’s roots when the request is relayed (ROOT-01/04).
  • Value to mcpx. − deprecated.
  • Effort. S — done.
  • Risk. low.
  • Detail. The embedded request’s _meta is a plain MetaObject, not RequestMetaObject, so no protocol version or capabilities are required inside it. opencode v2’s code says roots are “Legacy era only: … modern servers cannot request them”, yet the same client registers a roots/list handler and its SDK auto-fulfils MRTR input requests from registered handlers (mrtr.md, MRTR-18); which of the two holds on a modern v2 connection was not verified.
  • Sources. 2026-07-28/client/roots.mdx:8 “The Roots feature is deprecated as of protocol version”; schema/2026-07-28/schema.ts:2718 “export interface ListRootsRequest {”; schema/2026-07-28/schema.ts:2742 “export interface ListRootsResult {”; internal/mcpclient/modern.go:116-120; v2:packages/core/src/mcp/client.ts:144; v2:packages/core/src/mcp/client.ts:155.

ROOT-04 Server asks its host for roots

  • What. Any server may ask its client for roots (a roots/list request in legacy, a ListRootsRequest input request in 2026). A proxy can use that to learn the host’s workspace and pass it on to upstreams.
  • Where. Every revision (deprecated in 2026).
  • mcpx @ 2a93f3f64. rootsViaBroker relays roots/list to the host client whose call was interrupted; without a calling client, the client answers from its configured roots (internal/daemon/hooks.go:373-409; internal/mcpclient/modern.go:342-359).
  • Value to mcpx. + med: upstreams can learn the workspace of the client whose call they are serving.
  • Effort. M — done.
  • Risk. low: a host with no declared roots is not sent a roots request.
  • Detail. The relay is request-scoped; mcpx does not broadcast one client’s roots to other pooled clients.
  • Sources. internal/daemon/hooks.go:373-409; internal/mcpclient/relay_caps_test.go:78-134; internal/mcpserver/relay_test.go:149-152; internal/mcpclient/modern.go:342-359; v1:packages/opencode/src/mcp/index.ts:78.

ROOT-05 Per-request roots _meta key

  • What. SEP-2575 (stateless MCP) lists roots among the fields carried per request in _meta, next to protocolVersion, clientInfo, logLevel and clientCapabilities. The 2026-07-28 specification defines no io.modelcontextprotocol/roots key; roots arrive only as MRTR roots/list input requests.
  • Where. SEP text only.
  • mcpx @ 05c78b2. Neither sends nor reads such a key.
  • Value to mcpx. − moot: follow the specification, not the SEP.
  • Effort. S.
  • Risk. low: a server built from the SEP text might look for a key no client sends.
  • Detail. The reserved _meta key table in 2026 has protocolVersion, clientInfo, clientCapabilities, logLevel and subscriptionId, no roots.
  • Sources. seps/2575-stateless-mcp.md:760 “(protocolVersion, clientInfo, roots, logLevel, clientCapabilities)”; 2026-07-28/basic/index.mdx:356.

Sampling

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-11-01T16:20:00-06:00
increment:    1
status:       standard
tags:         area:compare
description:  sampling/createMessage fields and capabilities per revision, tool use in sampling, its 2026 deprecation, and how mcpx relays it

Sampling lets a server ask the client’s model for a completion: nearly unchanged from 2024-11-05 to 2025-06-18, given tool use and sub-capabilities in 2025-11-25, and deprecated in 2026-07-28, where it survives only as an embedded input request (mrtr.md); sampling as a client-hosted task (2025-11-25 only) is in tasks.md. mcpx has no model of its own, so sampling is a relay: upstream requests become broker rows for an agent to answer. The client declares sampling only when a request handler exists (SMP-09); host-bound relays check sampling.tools and sampling.context (SMP-11).

Status column: SMP-03/09/11 are current (audited at 2a93f3f64); other rows retain the mcpx @ 05c78b2 snapshot. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
SMP-01sampling/createMessage request, result and baseline fields2024-11-05 has 2026-07-28 deprecates mcpx has, others don't24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 dep.; opencode v1/v2 ✗, lootbox ✗✓ — relayed both directions through the broker+ lowSlow
SMP-02includeContext other than none soft-deprecated, then deprecated2025-11-25 deprecates 2026-07-28 deprecatesvalues in all five; gated by sampling.context from 25-11✓ — never declares sampling.context upstream− deprecatedSlow
SMP-03tools and toolChoice in sampling requests2025-11-25 has 2026-07-28 deprecates25-11 ✓ · 26-07 dep.✓ — not declared upstream; undeclared requests are refused, and host relay requires sampling.tools (modern.go:142-179; conn.go:572-581)+ lowMlow
SMP-04stopReason: "toolUse"2025-11-25 has25-11 ✓ · 26-07 ✓; open string in alln/a — never inspected− mootSlow
SMP-05SamplingMessage.content may be an array2025-11-25 has24-11..25-06 single · 25-11 single or array · 26-07 samen/a — relayed raw; broker decoding not examined+ lowSlow
SMP-06Tool-result messages hold only tool_result blocks, paired by id2025-11-25 has25-11 ✓ · 26-07 ✓ (dep.)n/a — mcpx runs no tool loop− mootSlow
SMP-07Sampling feature deprecated (SEP-2577)2026-07-28 deprecates26-07 dep.; removable from 2027-07-28✓ — relay exists; nothing new to build− deprecatedSlow
SMP-082026 CreateMessageRequest embedded; no _meta; result not a Result2026-07-28 has26-07 only✓ — decoded per inputRequests key+ lowSlow
SMP-09Client declares sampling only when something can answer2026-07-28 deprecatesspec: declare what you support; mcpx: handler-gated✓ — declared iff a request handler is configured (modern.go:120-123; daemon/hooks.go:82)+ medSmed
SMP-10Upstream sampling answered by an agent through the brokermcpx has, others don't 2026-07-28 deprecatesmcpx onlypartial — decline becomes an error; role/model filled in+ lowSmed
SMP-11Relaying sampling to a host checks declared sub-capabilities2025-11-25 has25-11 ✓ · 26-07 ✓✓ — tools require sampling.tools; context is dropped without sampling.context (conn.go:561-581,696-703)+ lowSlow

SMP-01 sampling/createMessage request, result and baseline fields

  • What. A server asks the client’s model for a completion: messages, modelPreferences (hints[], costPriority, speedPriority, intelligencePriority), systemPrompt, includeContext, temperature, maxTokens (required), stopSequences, metadata. The result is a SamplingMessage plus model and stopReason.
  • Where. All five revisions; deprecated in 2026-07-28. opencode v1 comments sampling out and v2 does not declare it; neither registers a handler. lootbox declares no capabilities.
  • mcpx @ 05c78b2. Relayed both ways: an upstream’s request goes to a downstream handler, which in the daemon is the broker (internal/mcpclient/modern.go:121-128); host-bound requests are gated by CanSample (internal/mcpserver/conn.go:211).
  • Value to mcpx. + low: the relay exists; the feature is deprecated, so it should not grow.
  • Effort. S — done.
  • Risk. low.
  • Detail. metadata is retyped from object to JSONObject in 2026. ModelHint.name is a substring hint, not an exact model id. opencode v2 plugins could answer a forwarded sampling request with ctx.generate.text, but only as a flattened prompt string with no messages, system prompt or tools.
  • Sources. schema/2024-11-05/schema.ts:762 “sampling/createMessage”; schema/2024-11-05/schema.ts:784 “maxTokens: number;”; schema/2026-07-28/schema.ts:2140 “metadata?: JSONObject;”; internal/mcpclient/modern.go:121-128; internal/mcpserver/conn.go:211; v1:packages/opencode/src/mcp/index.ts:42 “// sampling: {},”; v2:packages/core/src/mcp/client.ts:142; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153.

SMP-02 includeContext other than none soft-deprecated, then deprecated

  • What. includeContext: "none" | "thisServer" | "allServers" exists in every revision. 2025-11-25 soft-deprecates the two non-none values and gates them on sampling.context (servers SHOULD send only none or omit it otherwise). 2026 reclassifies them as Deprecated under SEP-2596, removable no later than Sampling itself.
  • Where. Values in all five; soft-deprecated in 2025-11-25; Deprecated in 2026-07-28.
  • mcpx @ 05c78b2. Never declares sampling.context upstream (internal/mcpclient/modern.go:55), so conforming upstreams send only none. That is right for a proxy, which has no “context from other servers” to inject. The host direction is SMP-11.
  • Value to mcpx. − deprecated: nothing to build.
  • Effort. S.
  • Risk. low: SHOULD-level only, so a server may still send the other values and the client MAY ignore them.
  • Detail. Changelog-silent: the 2025-11-25 changelog mentions neither the soft-deprecation nor sampling.context; the 2026 deprecated registry dates it to 2025-11-25. The 2024-11-05 and 2025-06-18 sampling prose never mention includeContext at all; it is schema-only there.
  • Sources. schema/2024-11-05/schema.ts:776 “includeContext?:”; schema/2025-11-25/schema.ts:1592 “Values "thisServer" and "allServers" are soft-deprecated.”; schema/2025-11-25/schema.ts:332 “context?: object;”; 2025-11-25/client/sampling.mdx:69 “With context inclusion support (soft-deprecated):”; 2026-07-28/deprecated.mdx:30; internal/mcpclient/modern.go:120-123.

SMP-03 tools and toolChoice in sampling requests

  • What. A server may pass tools: Tool[] (scoped to that request; they need not be registered tools) and toolChoice{mode: "auto" | "required" | "none"}. Clients MUST declare sampling.tools to receive them, and MUST return an error if they arrive anyway; servers MUST NOT send them to clients lacking it.
  • Where. 2025-11-25; deprecated with Sampling in 2026-07-28.
  • mcpx @ 2a93f3f64. Not declared upstream (internal/mcpclient/modern.go:120-123), so conforming upstreams do not send tools to mcpx. If a non-conforming upstream does, mcpx refuses tools/toolChoice with -32602 (internal/mcpclient/modern.go:142-179). Toward hosts, a tool-enabled request is offered only to a host that declared sampling.tools (internal/mcpserver/conn.go:572-581).
  • Value to mcpx. + low: only matters for an agentic-sampling relay, of a deprecated feature.
  • Effort. M — tool-loop semantics (SMP-05, SMP-06).
  • Risk. low while undeclared.
  • Detail. toolChoice defaults to {mode: "auto"}. The prose gate (server MUST NOT send) and the schema gate (client MUST error) apply to opposite ends; a proxy sits at both.
  • Sources. schema/2025-11-25/schema.ts:1615 “tools?: Tool[];”; schema/2025-11-25/schema.ts:1636 “mode?: "auto" | "required" | "none";”; schema/2025-11-25/schema.ts:336 “tools?: object;”; 2025-11-25/client/sampling.mdx:40 “Clients MUST declare support for tool use via the”; 2025-11-25/changelog.mdx:18 “Add tool calling support to sampling via”; internal/mcpclient/modern.go:142-179; internal/mcpserver/conn.go:572-581.

SMP-04 stopReason: "toolUse"

  • What. A new standard stop reason for “the model wants to call tools”. Earlier standard values are endTurn, stopSequence, maxTokens.
  • Where. 2025-11-25 and 2026-07-28. The field is an open string in every revision.
  • mcpx @ 05c78b2. Not inspected; the broker answer path does not fill stopReason (it is optional; see SMP-10).
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None: open string.
  • Detail. Only meaningful together with SMP-03.
  • Sources. schema/2024-11-05/schema.ts:804 “stopReason?:”; schema/2025-11-25/schema.ts:1673 “"toolUse" | string;”.

SMP-05 SamplingMessage.content may be an array

  • What. A single content block through 2025-06-18; SamplingMessageContentBlock | SamplingMessageContentBlock[] from 2025-11-25. Because CreateMessageResult extends SamplingMessage, results may be arrays too.
  • Where. 2025-11-25 and 2026-07-28.
  • mcpx @ 05c78b2. Upstream requests are relayed as raw params on the 2026 path (internal/mcpserver/ask.go:256-266); how the broker and the answering agent handle array content was not examined.
  • Value to mcpx. + low: a relay must not assume one block.
  • Effort. S.
  • Risk. low: decoding an array as an object fails.
  • Detail. Not in the 2025-11-25 changelog. Allowed block types grow from text and image (2024-11-05) to text, image, audio, tool_use and tool_result (2025-11-25); the content-types register owns the blocks themselves.
  • Sources. schema/2025-06-18/schema.ts:1090 “content: TextContent | ImageContent | AudioContent;”; schema/2025-11-25/schema.ts:1683 “content: SamplingMessageContentBlock | SamplingMessageContentBlock[];”.

SMP-06 Tool-result messages hold only tool_result blocks, paired by id

  • What. A user message containing tool_result blocks MUST contain only tool results, and every assistant message with tool_use blocks MUST be followed by a user message of only matching tool_results.
  • Where. 2025-11-25 and 2026-07-28 (deprecated).
  • mcpx @ 05c78b2. mcpx runs no sampling tool loop, so nothing to check.
  • Value to mcpx. − moot: needed only if mcpx relayed tool loops.
  • Effort. S.
  • Risk. low: -32602 from strict clients if violated.
  • Detail. The 2026 schema still lists “Missing tool result or tool results mixed with other content” as an InvalidParamsError case for sampling, but under MRTR a client has no way to return an error for one input request (mrtr.md, MRTR-17).
  • Sources. 2025-11-25/client/sampling.mdx:372 “it MUST contain ONLY tool results.”; 2025-11-25/client/sampling.mdx:430 “MUST be followed by a user message”; schema/2026-07-28/schema.ts:371 “Missing tool result or tool results mixed with other content”.

SMP-07 Sampling feature deprecated (SEP-2577)

  • What. The capability, request, result and all sampling types are @deprecated in 2026-07-28; the migration is to call LLM provider APIs directly.
  • Where. 2026-07-28; functional through at least 2027-07-28.
  • mcpx @ 05c78b2. The relay exists and is declared whenever a handler is installed (internal/mcpclient/modern.go:54-56).
  • Value to mcpx. − deprecated: keep what exists, do not grow it.
  • Effort. S.
  • Risk. None.
  • Detail. Roots and Logging are deprecated by the same SEP; Elicitation is not.
  • Sources. 2026-07-28/client/sampling.mdx:8 “The Sampling feature is deprecated as of protocol version”; 2026-07-28/deprecated.mdx:27 “Sampling”.

SMP-08 2026 CreateMessageRequest embedded; no _meta; result not a Result

  • What. In 2026 CreateMessageRequest no longer extends JSONRPCRequest; its params extend nothing (no _meta, so no progressToken); CreateMessageResult extends SamplingMessage only (no resultType, no _meta).
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The client decodes {method, params} per inputRequests key (internal/mcpclient/modern.go:140-143).
  • Value to mcpx. + low: matches.
  • Effort. S.
  • Risk. None.
  • Detail. None of this is in the changelog; it follows from MRTR. A long sampling request therefore has no progress channel in 2026.
  • Sources. schema/2026-07-28/schema.ts:2185 “export interface CreateMessageRequest {”; schema/2026-07-28/schema.ts:2210 “export interface CreateMessageResult extends SamplingMessage {”; schema/2025-11-25/schema.ts:1656 “export interface CreateMessageResult extends Result, SamplingMessage {”; internal/mcpclient/modern.go:140-143.

SMP-09 Client declares sampling only when something can answer

  • What. A client should declare only what it supports. mcpx declares sampling only when its server-request handler exists.
  • Where. Spec rule in every revision; mcpx’s capabilities() gates the field on a configured handler.
  • mcpx @ 2a93f3f64. capabilities() adds sampling iff OnServerRequest is non-nil (internal/mcpclient/modern.go:94-123); the daemon installs its broker-backed handler (internal/daemon/hooks.go:82,155-174). The handler turns requests into broker work; an unanswered request may still expire. Thus declaration reflects configured support, not whether an agent is connected at that instant.
  • Value to mcpx. − done: the declaration follows handler availability.
  • Effort. S — done.
  • Risk. med: a sampling server waits elicit.ttl (120 s) per call and gets an error when nobody answers.
  • Detail. The capability check reports configured support. A broker request can remain open until a handler answers or the request expires; lack of an immediately connected agent does not remove the configured handler.
  • Sources. internal/mcpclient/modern.go:94-123; internal/daemon/hooks.go:82,155-174,280-328.

SMP-10 Upstream sampling answered by an agent through the broker

  • What. An upstream’s sampling/createMessage becomes a broker row for the agent audience (the opencode plugin can answer it with the session’s model). An accept is returned as a CreateMessageResult with role: "assistant" and model: "unknown" filled in if missing; any other answer becomes a JSON-RPC error.
  • Where. mcpx only; neither opencode version nor lootbox handles sampling at all.
  • mcpx @ 05c78b2. internal/daemon/hooks.go:180-232.
  • Value to mcpx. + low: lets a deprecated feature work for servers that still depend on it.
  • Effort. S — done.
  • Risk. med: without an answerer, 120 s of waiting per call (SMP-09).
  • Detail. Sampling has no decline/cancel result, so mapping a declined broker row to an error is the only option in legacy; in 2026 it is also the only option, since MRTR has no per-input error (MRTR-17). stopReason is not filled; the schema makes it optional.
  • Sources. internal/daemon/hooks.go:215-217 “the sampling request was”; internal/daemon/hooks.go:180-232.

SMP-11 Relaying sampling to a host checks declared sub-capabilities

  • What. A tools/toolChoice request must not reach a host that did not declare sampling.tools; non-none includeContext must not be relayed unless the host declared sampling.context. The 2025-06-18 schema has no sampling.tools sub-capability; a conforming client from that revision cannot declare it.
  • Where. The sub-capabilities are 2025-11-25 and later.
  • mcpx @ 2a93f3f64. A tool-bearing question is sendable only when the host declared sampling.tools (internal/mcpserver/conn.go:561-581); paramsFor drops includeContext unless the host declared sampling.context (internal/mcpserver/conn.go:652-704). The host-facing gate is declaration-based, not a second explicit version predicate; a conforming pre-2025-11-25 peer cannot declare these sub-keys. In the reverse direction, sampling payload/result validation uses the negotiated revision (internal/daemon/hooks.go:295-303,362-368).
  • Value to mcpx. − done: no sampling fields beyond the host’s declared sub-capabilities are relayed.
  • Effort. S.
  • Risk. low: tools questions that are not sendable remain on the broker path; unsupported context is omitted.
  • Detail. It needs a non-conforming upstream to send tools to mcpx because its upstream declaration is bare sampling: {}; mcpx refuses those requests. The host-side sub-capability checks protect the other end of the relay.
  • Sources. internal/mcpserver/conn.go:313-319,561-581,652-704; internal/mcpclient/modern.go:142-179; internal/daemon/hooks.go:295-303,362-368; schema/2025-11-25/schema.ts:327-336.

Tasks

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  2025-11-25 core tasks versus the 2026 io.modelcontextprotocol/tasks extension, difference by difference, and what mcpx implements

A task turns a long request into a handle a client can poll, cancel and collect later. The 2025-11-25 core and 2026 extension have different wire formats. Task observations below reflect phases 1–2 of #1554; other observations retain their original snapshot. Limits, cancellation, audit, ID entropy and upstream input relay remain open questions on #1554.

Status column: updated task rows are current; other rows are mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpxValueEffortRisk
TASK-01Tasks leave the core and become an extension2025-11-25 has 2026-07-28 removes has better replacement specs conflict25-11 core · 26-07 extension onlySeparate legacy and extension paths (internal/mcpserver/tasks.go:maybeTask; internal/mcpserver/server.go:capabilities).+ highLhigh
TASK-02Client-hosted tasks (sampling, elicitation) removed; tools/call only2025-11-25 has 2026-07-28 removes25-11 server + client · 26-07 server only; opencode, lootbox declare none✓ — tools/call only; no client tasks declared− mootSlow
TASK-03Opt-in moves from params.task to the server’s discretion2026-07-28 removes has better replacement25-11 client asks · 26-07 server decidesLegacy opt-in; modern server decides (internal/mcpserver/tasks.go:maybeTask).+ medMmed
TASK-04tasks.list declared without requestor binding2025-11-25 has mcpx missing25-11 onlyLists connection-owned tasks only; unowned tasks remain readable by ID (internal/mcpserver/tasks.go:visible, handleTask; #1554).+ medSmed
TASK-05CreateTaskResult: {task} → flat Task with resultType: "task"2026-07-28 has25-11 nested · 26-07 flatLegacy {task}; modern flat resultType: "task" (internal/mcpserver/tasks.go:maybeTask, created; internal/mcpclient/tasks.go:finishModernTask).+ highShigh
TASK-06Task.ttl → ttlMs2026-07-28 has25-11 ttl · 26-07 ttlMsLegacy ttl, modern ttlMs (internal/tasks/tasks.go:Task; internal/mcpserver/tasks.go:modernTask; internal/mcpclient/tasks.go:modernTaskResult.UnmarshalJSON).+ lowSlow
TASK-07Task.pollInterval → pollIntervalMs2026-07-28 has25-11 pollInterval · 26-07 pollIntervalMsLegacy pollInterval, modern pollIntervalMs (internal/tasks/tasks.go:Task; internal/mcpserver/tasks.go:modernTask; internal/mcpclient/tasks.go:finishModernTask).+ lowSlow
TASK-08tasks/get returns a DetailedTask with inline result or error2026-07-28 has specs conflict25-11 status only · 26-07 detailedModern get inlines terminal result/error or pending input (internal/mcpserver/tasks.go:detailedNow, handleModernTask; internal/mcpclient/tasks.go:finishModernTask).+ medMmed
TASK-09Blocking tasks/result removed2026-07-28 removes has better replacement25-11 only; extension: -32601Modern method-not-found; legacy result remains (internal/mcpserver/tasks.go:handleModernTask, handleTask).+ lowSlow
TASK-10tasks/list removed2026-07-28 removes has no replacement25-11 onlyModern method-not-found; legacy list scoped to connection (internal/mcpserver/tasks.go:handleModernTask, handleTask).+ lowSmed
TASK-11tasks/update added for mid-task input2026-07-28 has26-07 extension onlyModern update answers outstanding asked-task questions, ignoring unknown keys (internal/mcpserver/tasks.go:updateAsked, handleModernTask).+ medMmed
TASK-12tasks/cancel result: Task → empty acknowledgement2026-07-28 has mcpx missing25-11 Task, MUST cancel · 26-07 empty ack, cooperativeModern empty acknowledgement, legacy snapshot; modern cancellation still forces terminal state (internal/mcpserver/tasks.go:handleTask, handleModernTask; internal/tasks/tasks.go:CancelWithReason; #1554).+ lowSlow
TASK-13tasks/cancel on a finished task2025-11-25 has25-11 MUST -32602 · 26-07 empty ackLegacy terminal cancel rejected; modern acknowledged (internal/mcpserver/tasks.go:handleTask, handleModernTask).+ lowSlow
TASK-14Status push: notifications/tasks/status → notifications/tasks on listen2025-11-25 has 2026-07-28 has25-11 unsolicited · 26-07 listen filter taskIdsModern listen supports task notifications (internal/tasks/tasks.go:Watch; internal/mcpserver/listen_tasks_test.go:TestListenDeliversTaskStatusNotifications).+ lowMlow
TASK-15io.modelcontextprotocol/related-task _meta2025-11-25 has 2026-07-28 removes mcpx missing25-11 MUST · 26-07 —Legacy result and task-related input requests carry related-task metadata (internal/mcpserver/tasks.go:handleTask; internal/conformance/stateful_test.go:TestStatefulServer).+ lowSlow
TASK-16io.modelcontextprotocol/model-immediate-response hint2025-11-25 has 2026-07-28 removes has no replacement25-11 onlyn/a — never sent− mootSlow
TASK-17failed versus isError semantics inverted2026-07-28 has specs conflict25-11 isError → failed · 26-07 isError → completedLegacy store marks isError failed; modern detailed result reports completed (internal/tasks/tasks.go:finish; internal/mcpserver/tasks.go:detailedNow).+ lowSlow
TASK-18Mid-task input: tasks/result side channel → inputRequests + tasks/update2026-07-28 has mcpx missing25-11 side channel · 26-07 polledServer relays asked-task input via get/update; upstream input_required yields a named error instead (internal/mcpserver/tasks.go:driveAsked, updateAsked; internal/mcpclient/tasks.go:TaskInputRequiredError; #1554).+ medMmed
TASK-19Progress on tasks: supported → not supported2025-11-25 has 2026-07-28 removes25-11 token lives with task · 26-07 nonen/a — mcpx sends no progress− mootSlow
TASK-20Durable creation before CreateTaskResult2026-07-28 has26-07 extension onlyInjected SQLite store saves before returning and can reopen; database write errors are ignored (internal/tasks/tasks.go:StartWithDeadline, saveTaskLocked, Open; internal/mcpserver/tasks.go:tasks).+ lowLlow
TASK-21HTTP routing: Mcp-Name = taskId on tasks/*2026-07-28 has26-07 extension onlyClient sets task routing headers (internal/mcpclient/headers_tasks_test.go:TestStandardHeadersTaskID); server header validation is a separate transport concern.+ lowSlow
TASK-22Undeclared task support: SHOULD NOT create → -320212026-07-28 has25-11 SHOULD NOT · 26-07 MUST -32021Modern undeclared clients get -32021 for extension methods and required tools (internal/mcpserver/tasks.go:missingTasks, handleTask, maybeTask).+ lowSlow
TASK-23Task IDs unguessable2025-11-25 has 2026-07-28 has25-11 MUST when unbound · 26-07 MUSTOpen: 64 random bits (internal/tasks/tasks.go:NewID); entropy is an open question on #1554; #2869 is unmerged.+ lowSmed
TASK-24Tasks bound to the requestor; checked on every request2025-11-25 has mcpx missing25-11 MUST with auth context · 26-07 MUSTConnection-owned legacy tasks isolated; unowned tasks accessible by ID and ownership is in memory (internal/mcpserver/tasks.go:startTask, visible, handleTask; #1554).+ medShigh
TASK-25Reserved names tasks/, notifications/tasks/, resultType: "task"2026-07-28 has specs conflict26-07 extension onlyn/a — nothing reserved clashes− mootSlow
TASK-26MRTR rounds resolved before a CreateTaskResult2026-07-28 has26-07 + extensionServer answers sendable questions inline before returning a task (internal/mcpserver/tasks.go:askTask, maybeTask).+ lowMlow
TASK-27One task store across MCP and /v1mcpx onlyDaemon injects its task store into MCP (internal/daemon/ops.go:TaskStore; internal/mcpserver/tasks.go:tasks; internal/cli/serve.go:MCPServer, internal/cli/root.go).+ medSmed

TASK-01 Tasks leave the core and become an extension

  • What. The 2025-11-25 core “experimental” tasks (tasks/* methods, Task* types, tasks capabilities, Tool.execution) are absent from the 2026 schema. The redesigned feature is io.modelcontextprotocol/tasks, negotiated through capabilities.extensions. SEP-2663 says it is not wire-compatible with 2025-11-25 and must not be enabled under 2025-11-25.
  • Where. Core in 2025-11-25; extension only with 2026-07-28.
  • mcpx (current). Separate legacy and extension paths (internal/mcpserver/tasks.go:maybeTask; internal/mcpserver/server.go:capabilities). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + high: a real extension implementation would let long upstream calls survive a 2026 host’s disconnect; declaring what is not delivered is the build brief’s “declared but not delivered” bug class.
  • Effort. L — new result shape, update and poll flow, notifications; the rows below are the parts.
  • Risk. high: 2026 hosts that trust the advertisement send and expect extension shapes mcpx does not produce.
  • Detail. Dual support means parallel code paths, not one shape with renames. SEP-2663 calls the 2026 revision 2026-06-30, presumably a pre-release name. The capability-level leaks (extensions to 2025-11-25 hosts, core tasks to 2026 hosts) are also registered in the capabilities and lifecycle registers.
  • Sources. 2026-07-28/changelog.mdx:22 “Move experimental tasks out of the core protocol and into an official extension”; seps/2663-tasks-extension.md:944 “is not wire-compatible with this extension.”; seps/2663-tasks-extension.md:17; docs/extensions/tasks/overview.mdx:7; internal/mcpserver/server.go:1404-1417; internal/mcpserver/tasks.go:88-126; internal/mcpserver/revisions.go:90.

TASK-02 Client-hosted tasks (sampling, elicitation) removed; tools/call only

  • What. 2025-11-25 lets a server task-augment tools/call and lets a client host tasks for server-sent sampling/createMessage and elicitation/create (client tasks.requests.*, tasks.list, tasks.cancel). The extension supports only tools/call; a CreateTaskResult for any other method MUST be treated as invalid. Client-hosted tasks cease to exist, because polling one would be a server-to-client request.
  • Where. 2025-11-25 only for the client side. opencode v1 comments tasks out, v2 does not declare it, and lootbox declares no capabilities.
  • mcpx @ 05c78b2. Server: tools/call only (internal/mcpserver/server.go:477-500). Client: no tasks declared upstream (internal/mcpclient/modern.go:46-58).
  • Value to mcpx. − moot: already aligned; implementing client-hosted tasks would be work for one revision.
  • Effort. S.
  • Risk. None.
  • Detail. In 2025-11-25 CreateMessageRequestParams and ElicitRequestFormParams extend TaskAugmentedRequestParams; in 2026 they extend nothing. The extension SHOULD be designed to accommodate more methods later.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:71; schema/2025-11-25/schema.ts:346 “tasks?: {”; schema/2025-11-25/schema.ts:1578 “export interface CreateMessageRequestParams extends TaskAugmentedRequestParams {”; schema/2026-07-28/schema.ts:2104 “export interface CreateMessageRequestParams {”; seps/2663-tasks-extension.md:35 “Client-hosted tasks are no longer expressible.”; seps/2663-tasks-extension.md:87; v1:packages/opencode/src/mcp/index.ts:48 “// tasks: {},”; v2:packages/core/src/mcp/client.ts:142; .lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153.

TASK-03 Opt-in moves from params.task to the server’s discretion

  • What. 2025-11-25 clients add params.task: {ttl?} to ask for task execution. The extension removes task; servers MUST ignore it, and the server alone decides per request whether to return a task to a client that declared the extension.
  • Where. task param in 2025-11-25 only.
  • mcpx (current). Legacy opt-in; modern server decides (internal/mcpserver/tasks.go:maybeTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: server-chosen tasks would let mcpx hand back a handle for a slow upstream call without the host asking, which is the extension’s whole model.
  • Effort. M.
  • Risk. med: a 2026 host never sends task, so mcpx’s task path is unreachable for exactly the hosts it advertises the extension to.
  • Detail. The 2026 changelog’s “without per-request opt-in” still requires the extension in each request’s clientCapabilities; the overview says “Never return a task to a client that did not declare support”. The two read as contradictory until “opt-in” is taken to mean the removed task param.
  • Sources. schema/2025-11-25/schema.ts:44 “task?: TaskMetadata;”; seps/2663-tasks-extension.md:85 “The server is the sole decider; clients do not signal task preference on the request itself.”; docs/extensions/tasks/overview.mdx:231-233; internal/mcpserver/tasks.go:40-56.

TASK-04 tasks.list declared without requestor binding

  • What. A 2025-11-25 receiver that cannot identify requestors SHOULD NOT declare tasks.list, because listing exposes every task’s metadata to anyone.
  • Where. 2025-11-25 only.
  • mcpx (current). Lists connection-owned tasks only; unowned tasks remain readable by ID (internal/mcpserver/tasks.go:visible, handleTask; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: removes cross-host metadata exposure.
  • Effort. S — stop declaring it, or bind first (TASK-24).
  • Risk. med: on a shared daemon any host sees every host’s task ids and can collect their results.
  • Detail. Task ids are 64 random bits (TASK-23), so listing is the easy path to someone else’s result.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:876 “SHOULD NOT declare the”; internal/mcpserver/server.go:1412; internal/mcpserver/tasks.go:89-95; wire W23.

TASK-05 CreateTaskResult: {task} → flat Task with resultType: "task"

  • What. 2025-11-25 CreateTaskResult is { task: Task }. The extension’s is Result & Task with resultType: "task", a value the extension reserves and advertises.
  • Where. As stated.
  • mcpx (current). Legacy {task}; modern flat resultType: "task" (internal/mcpserver/tasks.go:maybeTask, created; internal/mcpclient/tasks.go:finishModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + high: without it no 2026 host can use an mcpx task.
  • Effort. S for the shape, given the rest of TASK-01.
  • Risk. high: a 2026 host reads resultType: "complete" and treats {task: …} as an empty tool result.
  • Detail. resultType values beyond core are legal only when advertised through capabilities. As a client, mcpx treats any resultType other than input_required as complete (internal/mcpclient/client.go:665), so a modern upstream’s "task" would be parsed as a tool result too.
  • Sources. schema/2025-11-25/schema.ts:1392 “task: Task;”; seps/2663-tasks-extension.md:262 “type CreateTaskResult = Result & Task;”; seps/2663-tasks-extension.md:128; docs/extensions/tasks/overview.mdx:54-56; internal/mcpserver/server.go:499-500; 2026-07-28/basic/index.mdx:83; wire W22.

TASK-06 Task.ttl → ttlMs

  • What. Renamed; still milliseconds, null for unlimited. The extension adds that it MAY change over the task’s lifetime. The request-side TaskMetadata.ttl disappears with the task param.
  • Where. ttl in 2025-11-25; ttlMs in the extension.
  • mcpx (current). Legacy ttl, modern ttlMs (internal/tasks/tasks.go:Task; internal/mcpserver/tasks.go:modernTask; internal/mcpclient/tasks.go:modernTaskResult.UnmarshalJSON). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: a rename.
  • Effort. S.
  • Risk. low: extension clients see no TTL.
  • Detail. The TTL comes from the client’s task.ttl or plumbing.taskTTL (10 m).
  • Sources. schema/2025-11-25/schema.ts:1378 “ttl: number | null;”; seps/2663-tasks-extension.md:168 “ttlMs: number | null;”; internal/tasks/tasks.go:36.

TASK-07 Task.pollInterval → pollIntervalMs

  • What. Renamed. Servers MAY rate-limit clients that poll faster.
  • Where. pollInterval in 2025-11-25; pollIntervalMs in the extension.
  • mcpx (current). Legacy pollInterval, modern pollIntervalMs (internal/tasks/tasks.go:Task; internal/mcpserver/tasks.go:modernTask; internal/mcpclient/tasks.go:finishModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. low: extension clients see no interval.
  • Detail. None beyond the rename.
  • Sources. schema/2025-11-25/schema.ts:1383 “pollInterval?: number;”; seps/2663-tasks-extension.md:175 “pollIntervalMs?: number;”; internal/tasks/tasks.go:37.

TASK-08 tasks/get returns a DetailedTask with inline result or error

  • What. 2025-11-25 GetTaskResult = Result & Task, a status only. The extension returns status-discriminated variants: input_required carries inputRequests, completed carries result, failed carries error; resultType MUST be "complete" on it.
  • Where. As stated.
  • mcpx (current). Modern get inlines terminal result/error or pending input (internal/mcpserver/tasks.go:detailedNow, handleModernTask; internal/mcpclient/tasks.go:finishModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: one polling call gives everything, which suits hosts that reconnect.
  • Effort. M.
  • Risk. med: see TASK-01.
  • Detail. SEP-2663 contradicts itself: it requires "complete" here but its own execution-error examples show "resultType": "task".
  • Sources. schema/2025-11-25/schema.ts:1415 “export type GetTaskResult = Result & Task;”; seps/2663-tasks-extension.md:337 “type GetTaskResult = Result & DetailedTask;”; seps/2663-tasks-extension.md:340; seps/2663-tasks-extension.md:844 “"resultType": "task",”; internal/mcpserver/tasks.go:97-102.

TASK-09 Blocking tasks/result removed

  • What. 2025-11-25 tasks/result blocks until the task is terminal, returns its payload, and is where mid-task input requests travel. The extension removes it; clients calling it MUST get -32601, and results move into tasks/get.
  • Where. 2025-11-25 only.
  • mcpx (current). Modern method-not-found; legacy result remains (internal/mcpserver/tasks.go:handleModernTask, handleTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: accept-liberally is harmless as long as it is not advertised to 2026 hosts, which it is (CAP-26).
  • Effort. S.
  • Risk. low.
  • Detail. Blocking until the end is exactly what the redesign moved away from, because a blocking request is lost with its connection.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:465 “it MUST block the response until the task reaches a terminal status”; seps/2663-tasks-extension.md:949 “is removed; clients calling it”; internal/mcpserver/tasks.go:104-117.

TASK-10 tasks/list removed

  • What. Listing cannot be scoped without sessions, so the extension removes it with no replacement: clients must keep the task ids they were given.
  • Where. 2025-11-25 only.
  • mcpx (current). Modern method-not-found; legacy list scoped to connection (internal/mcpserver/tasks.go:handleModernTask, handleTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: stop serving it to hosts that declared the extension; for legacy hosts the fix is TASK-24.
  • Effort. S.
  • Risk. med: SEP-2663’s own argument, “a server cannot inadvertently leak the existence of one caller’s tasks to another”, applies directly to mcpx’s shared store.
  • Detail. 2025-11-25 required every task visible to tasks/get for a requestor to be listable for that requestor.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:490 “it MUST be retrievable via”; seps/2663-tasks-extension.md:957 “a server cannot inadvertently leak the existence of one caller’s tasks to another.”; internal/mcpserver/tasks.go:89-95; wire W23.

TASK-11 tasks/update added for mid-task input

  • What. tasks/update {taskId, inputResponses} answers a task’s outstanding inputRequests. The reply is an empty acknowledgement and eventually consistent; unknown or already-answered keys SHOULD be ignored; partial sets are allowed.
  • Where. Extension only.
  • mcpx (current). Modern update answers outstanding asked-task questions, ignoring unknown keys (internal/mcpserver/tasks.go:updateAsked, handleModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: input for long tasks without holding a request open; it maps directly onto the broker and Ask.Reply machinery MRTR already uses.
  • Effort. M.
  • Risk. med: extension hosts cannot answer mid-task questions.
  • Detail. Input-request keys MUST be unique over the task’s whole lifetime, stronger than MRTR’s per-request uniqueness.
  • Sources. seps/2663-tasks-extension.md:358 “tasks/update”; seps/2663-tasks-extension.md:352 “Each request key in”; docs/extensions/tasks/overview.mdx:36-39; internal/mcpserver/tasks.go:126 “return fail(codeMethodNotFound, "no method "+req.Method)”.

TASK-12 tasks/cancel result: Task → empty acknowledgement

  • What. 2025-11-25 returns the Task, MUST move it to cancelled before responding, and cancelled is sticky. The extension returns an empty acknowledgement; cancellation is cooperative and the task may still end in another terminal state.
  • Where. As stated.
  • mcpx (current). Modern empty acknowledgement, legacy snapshot; modern cancellation still forces terminal state (internal/mcpserver/tasks.go:handleTask, handleModernTask; internal/tasks/tasks.go:CancelWithReason; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: an acknowledgement is simpler for an asynchronous daemon.
  • Effort. S.
  • Risk. low.
  • Detail. In both designs notifications/cancelled MUST NOT be used to cancel a task.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:495 “MUST transition the task to”; seps/2663-tasks-extension.md:403 “type CancelTaskResult = Result; // empty acknowledgement”; seps/2663-tasks-extension.md:408 “Cancellation is cooperative”; internal/mcpserver/tasks.go:119-124.

TASK-13 tasks/cancel on a finished task

  • What. 2025-11-25: cancelling a task already completed, failed or cancelled MUST be rejected with -32602. The extension acknowledges with an empty result.
  • Where. 2025-11-25 MUST; extension changes it.
  • mcpx (current). Legacy terminal cancel rejected; modern acknowledged (internal/mcpserver/tasks.go:handleTask, handleModernTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. low.
  • Detail. Neither revision’s answer is what mcpx sends: 2025-11-25 wants an error, the extension an empty object.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:494 “MUST reject cancellation requests for tasks already in a terminal status”; internal/mcpserver/tasks.go:124 “return reply(snap)”.

TASK-14 Status push: notifications/tasks/status → notifications/tasks on listen

  • What. 2025-11-25 receivers MAY push notifications/tasks/status unsolicited. The extension renames it notifications/tasks, carries the full DetailedTask, and delivers it only on a subscriptions/listen stream whose filter lists taskIds (a field the extension adds to the core filter).
  • Where. As stated.
  • mcpx (current). Modern listen supports task notifications (internal/tasks/tasks.go:Watch; internal/mcpserver/listen_tasks_test.go:TestListenDeliversTaskStatusNotifications). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: saves hosts polling.
  • Effort. M — needs the listen stream to accept taskIds (notifications.md).
  • Risk. low: optional in both designs.
  • Detail. The overview page does not mention taskIds; only SEP-2663 does.
  • Sources. schema/2025-11-25/schema.ts:1496 “notifications/tasks/status”; seps/2663-tasks-extension.md:422 “notifications/tasks”; seps/2663-tasks-extension.md:435 “taskIds?: string[];”; docs/protocol.md:297-299 “mcpx polls task state rather than tracking it”; internal/mcpclient/client.go:478.

TASK-15 io.modelcontextprotocol/related-task _meta

  • What. 2025-11-25 requires every task-related request, notification and response (including the tasks/result response) to carry _meta["io.modelcontextprotocol/related-task"] = {taskId}. Neither the 2026 core nor SEP-2663 defines it.
  • Where. 2025-11-25 only.
  • mcpx (current). Legacy result and task-related input requests carry related-task metadata (internal/mcpserver/tasks.go:handleTask; internal/conformance/stateful_test.go:TestStatefulServer). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: lets a 2025-11-25 host tie a mid-task question to its task. The upstream direction would give mcpx exact attribution of questions on shared instances (elicitation.md, ELI-19).
  • Effort. S.
  • Risk. low.
  • Detail. For tasks/get, tasks/result and tasks/cancel, receivers MUST prefer the taskId param over this key. With no server-to-client requests in 2026 there is nothing left to associate.
  • Also recorded from the meta register. No related-task anywhere in internal/; tasks/result results and questions raised while a task runs carry none (internal/mcpserver/tasks.go:104-117). The tasks extension replaces it with inputRequests on tasks/get; see the tasks register.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:470 “All requests, notifications, and responses related to a task”; schema/2025-11-25/schema.ts:1332 “export interface RelatedTaskMetadata {”; schema/2025-11-25/schema.ts:1328; internal/mcpserver/tasks.go:104-117.

TASK-16 io.modelcontextprotocol/model-immediate-response hint

  • What. A non-binding, “provisional” _meta string on a 2025-11-25 CreateTaskResult that a host may show the model as an interim tool result. The extension has no such key.
  • Where. 2025-11-25 only.
  • mcpx @ 05c78b2. Never sent (no occurrence in internal/).
  • Value to mcpx. − moot: mcpx could synthesise its own interim text if it ever needed one.
  • Effort. S.
  • Risk. None.
  • Detail. None.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:175 “servers can provide an optional”.

TASK-17 failed versus isError semantics inverted

  • What. 2025-11-25: a tool result with isError: true puts the task in failed. Extension: failed MUST NOT be used for isError; that is completed with the result, and failed is only for JSON-RPC errors, carried in error.
  • Where. As stated.
  • mcpx (current). Legacy store marks isError failed; modern detailed result reports completed (internal/tasks/tasks.go:finish; internal/mcpserver/tasks.go:detailedNow). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: the extension’s rule is the cleaner one for a proxy.
  • Effort. S.
  • Risk. low: clients switching on status misreport tool errors between the two designs.
  • Detail. A dual-era server has to pick per revision.
  • Sources. schema/2025-11-25/schema.ts:1310 “For tool calls specifically, this includes cases where the tool call result has”; seps/2663-tasks-extension.md:835; internal/mcpserver/tasks.go:62-64.

TASK-18 Mid-task input: tasks/result side channel → inputRequests + tasks/update

  • What. 2025-11-25: on input_required the requestor SHOULD call tasks/result so the receiver can send elicitation/create or sampling on that stream. Extension: pending requests appear in tasks/get as inputRequests and are answered with tasks/update.
  • Where. As stated.
  • mcpx (current). Server relays asked-task input via get/update; upstream input_required yields a named error instead (internal/mcpserver/tasks.go:driveAsked, updateAsked; internal/mcpclient/tasks.go:TaskInputRequiredError; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: fits mcpx’s broker model, where questions are already data.
  • Effort. M.
  • Risk. med: a task whose upstream asks something sits at input_required with no way to answer through tasks.
  • Detail. The statuses themselves (working, input_required, completed, failed, cancelled) are the same five in both designs. The task-phase inputRequests key space is separate from any MRTR phase before it (TASK-26).
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:449 “When the requestor encounters the”; seps/2663-tasks-extension.md:184 “will include outstanding requests in the”; internal/mcpserver/tasks.go:69-73.

TASK-19 Progress on tasks: supported → not supported

  • What. 2025-11-25: the original progressToken stays valid for the task’s life, and notifications stop when it is terminal. Extension: notifications/progress and notifications/message are not supported on tasks and MUST NOT go on the listen stream for a task; statusMessage is the only progress channel left.
  • Where. As stated.
  • mcpx @ 05c78b2. Sends no progress at all (progress-cancellation.md).
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. SetTaskStatus takes a message, so statusMessage is available to mcpx already.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:483 “Task-augmented requests support progress notifications”; 2025-11-25/basic/utilities/progress.mdx:69; seps/2663-tasks-extension.md:511 “are not supported on tasks in general in this specification.”.

TASK-20 Durable creation before CreateTaskResult

  • What. New MUST in the extension: a server returns CreateTaskResult only once tasks/get for that id would succeed.
  • Where. Extension only.
  • mcpx (current). Injected SQLite store saves before returning and can reopen; database write errors are ignored (internal/tasks/tasks.go:StartWithDeadline, saveTaskLocked, Open; internal/mcpserver/tasks.go:tasks). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: satisfied in-process; restart durability is a separate, larger feature.
  • Effort. L for durability across daemon restarts.
  • Risk. low: tasks vanish on restart, which servers may do (they may purge), but it surprises hosts that persisted ids.
  • Detail. The extension also lets servers fail tasks after their TTL.
  • Sources. seps/2663-tasks-extension.md:302 “A server MUST NOT return”; internal/mcpserver/tasks.go:31-38.

TASK-21 HTTP routing: Mcp-Name = taskId on tasks/*

  • What. Over Streamable HTTP, tasks/get, tasks/update and tasks/cancel MUST set Mcp-Name to params.taskId, so a load balancer can route to the instance that owns the task.
  • Where. Extension only.
  • mcpx (current). Client sets task routing headers (internal/mcpclient/headers_tasks_test.go:TestStandardHeadersTaskID); server header validation is a separate transport concern. The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: needed if mcpx becomes a 2026 HTTP client of task-returning upstreams.
  • Effort. S.
  • Risk. low: misrouted polls against multi-instance upstreams.
  • Detail. The headers come from SEP-2243.
  • Sources. seps/2663-tasks-extension.md:515 “the client MUST set the”.

TASK-22 Undeclared task support: SHOULD NOT create → -32021

  • What. 2025-11-25: if the peer’s capabilities.tasks is undefined, a requestor SHOULD NOT create tasks. Extension: a server that can serve a request only as a task MUST return -32021 with requiredCapabilities.extensions["io.modelcontextprotocol/tasks"], and MUST do the same for tasks/get, tasks/update and tasks/cancel from clients that did not declare the extension.
  • Where. As stated.
  • mcpx (current). Modern undeclared clients get -32021 for extension methods and required tools (internal/mcpserver/tasks.go:missingTasks, handleTask, maybeTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: honest failure for 2026 clients.
  • Effort. S.
  • Risk. low.
  • Detail. Invalid or expired taskId → -32602 (MUST for tasks/get, SHOULD for update and cancel); mcpx already answers an unknown id with -32602. Accepting tasks/* from legacy clients is accept-liberally; the extension’s MUST applies only to 2026 clients.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:99 “attempt to create tasks during requests.”; seps/2663-tasks-extension.md:89 “the server MUST return an error with the code”; seps/2663-tasks-extension.md:799 “Servers MUST return this error for non-declaring clients issuing”; wire W22, W23.

TASK-23 Task IDs unguessable

  • What. Task ids may act as bearer tokens, so both designs require enough entropy: 2025-11-25 when tasks cannot be bound to an authorization context, the extension always.
  • Where. 2025-11-25 and the extension.
  • mcpx (current). Open: 64 random bits (internal/tasks/tasks.go:NewID); entropy is an open question on #1554; #2869 is unmerged. The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: 128 bits would remove the question.
  • Effort. S.
  • Risk. med: with no binding (TASK-24) the id is the only protection, and tasks/list hands ids out anyway (TASK-04).
  • Detail. 2025-11-25 also suggests shorter TTLs when binding is unavailable.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:875 “receivers MUST generate cryptographically secure task IDs”; seps/2663-tasks-extension.md:955 “Servers MUST generate them with sufficient entropy”; internal/tasks/tasks.go:86-90.

TASK-24 Tasks bound to the requestor; checked on every request

  • What. 2025-11-25: when an authorization context exists, receivers MUST bind tasks to it and reject others’ requests; without one they SHOULD document the limitation. Extension: authentication and authorization MUST be checked on every task request, and task inputRequests get the same trust model as direct ones.
  • Where. 2025-11-25 and the extension.
  • mcpx (current). Connection-owned legacy tasks isolated; unowned tasks accessible by ID and ownership is in memory (internal/mcpserver/tasks.go:startTask, visible, handleTask; #1554). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: isolation between hosts sharing one daemon.
  • Effort. S — key tasks by the connection binding now, by principal once /mcp has one.
  • Risk. high: result disclosure between hosts. /mcp has no authentication, so the 2025-11-25 MUST does not strictly apply, but the SHOULD to document it does, and the docs are silent.
  • Detail. The same binding question as MRTR’s requestState (mrtr.md, MRTR-08): a session id is what mcpx has, and 2026 removed it.
  • Sources. 2025-11-25/basic/utilities/tasks.mdx:871-876; seps/2663-tasks-extension.md:956 “Servers MUST perform authentication and authorization checks on each task-related request”; internal/mcpserver/tasks.go:34-36; wire W23.

TASK-25 Reserved names tasks/, notifications/tasks/, resultType: "task"

  • What. The extension reserves the tasks/ method prefix, the notifications/tasks/ notification prefix, and the "task" result discriminator.
  • Where. Extension only.
  • mcpx @ 05c78b2. Nothing of its own uses these names.
  • Value to mcpx. − moot.
  • Effort. S.
  • Risk. None.
  • Detail. The extension’s own notification is notifications/tasks, with no trailing slash, so it falls outside the notifications/tasks/ prefix it reserves.
  • Sources. seps/2663-tasks-extension.md:894 “prefix are reserved for this extension.”; seps/2663-tasks-extension.md:422.

TASK-26 MRTR rounds resolved before a CreateTaskResult

  • What. A tools/call may go through one or more input_required rounds and then return resultType: "task"; servers SHOULD resolve all MRTR exchanges synchronously first. The two phases have separate key spaces.
  • Where. 2026-07-28 with the extension.
  • mcpx (current). Server answers sendable questions inline before returning a task (internal/mcpserver/tasks.go:askTask, maybeTask). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + low: design guidance for when mcpx implements the extension.
  • Effort. M.
  • Risk. low.
  • Detail. SHOULD, not MUST.
  • Sources. seps/2663-tasks-extension.md:304 “resolve all MRTR exchanges synchronously before responding with a”.

TASK-27 One task store across MCP and /v1

  • What. internal/tasks says one store is shared so that a task started on one surface is visible from the other. In practice the daemon now injects its store into the MCP server.
  • Where. mcpx internal parity (config = /v1 = MCP).
  • mcpx (current). Daemon injects its task store into MCP (internal/daemon/ops.go:TaskStore; internal/mcpserver/tasks.go:tasks; internal/cli/serve.go:MCPServer, internal/cli/root.go). The assessment and source references below preserve the original 05c78b2 comparison; implementation-specific claims there are historical, not current.
  • Value to mcpx. + med: one view of background work.
  • Effort. S — the daemon hands its store to the lazily built MCP server.
  • Risk. med: a doc/code disagreement, and users look in the wrong place.
  • Detail. None beyond the above.
  • Sources. internal/tasks/tasks.go:10-12 “Two stores would be two”; internal/mcpserver/tasks.go:35 “s.taskStore = tasks.New()”; internal/daemon/ops.go:428-429.

Tools

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  Tool definition fields, JSON Schema rules, tools/list and CallToolResult per revision, and how mcpx and opencode handle each

Tools are the one MCP feature every host uses, and mcpx sits on both sides: it consumes upstream tool definitions to catalogue, type and call them, and serves its own tool surface to hosts. What matters most at 05c78b2 is that mcpx_call, the core proxy tool, drops the upstream isError and flattens every result to text (wire W20, W21), and that mcpx’s client drops icons, execution and _meta from upstream definitions while its own tools/list is 61 tools, not the promised ten. Content block types are in the content-types register, the invalid-arguments conflict (-32602 or isError; 2026 prose and schema disagree) in the errors register, ttlMs/cacheScope and per-connection stability in the pagination-caching register, and Mcp-Param-* header encoding in the transports register.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
TOOL-01Tool.name format: 1–128 chars, [A-Za-z0-9_.-], case-sensitive2025-11-25 has24-11 ✓ (unique id only) · 25-03 same · 25-06 same · 25-11 ✓ (rules) · 26-07 ✓✓ — own names comply+ lowSlow
TOOL-02aggregators SHOULD prefix a server id; serverInfo.name unreliable2026-07-28 has26-07 ✓ only✓ — namespaces by config key, not serverInfo.name+ medSlow
TOOL-03host-side names server_tool; opencode v2 caps at 64, no dotopencode v1 has opencode v2 hasopencode v1 ✓ · v2 ✓ (≤64)partial — served as mcpx, names double to mcpx_mcpx_*+ lowSlow
TOOL-04Tool.title and display precedence title → annotations.title → name2025-06-18 has24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — upstream title kept; own tools have none+ lowSlow
TOOL-05ToolAnnotations.title2025-03-26 has24-11 — · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — op tools only; the ten core tools lack it+ lowSlow
TOOL-06Tool.description optional; “hint to the model” wording from 2025-03-262024-11-05 hasall five ✓✓ — always sent+ lowSlow
TOOL-07inputSchema opens to any 2020-12 keyword; root type:"object" stays2024-11-05 has 2026-07-28 has24-11..25-11 ✓ (restricted) · 26-07 ✓ (any keyword)✓ — upstream schemas passed through raw+ medMmed
TOOL-08no-parameter tools: {type:"object", additionalProperties:false} recommended2025-11-25 has25-11 ✓ · 26-07 ✓✓ — used by every no-arg mcpx tool+ lowSlow
TOOL-09x-mcp-header on inputSchema properties; HTTP clients drop invalid tools2026-07-28 has mcpx missing26-07 ✓ only✗ — neither validated nor mirrored (#200)+ medMmed
TOOL-10JSON Schema 2020-12 is the default dialect2025-11-25 has25-11 ✓ · 26-07 ✓ · earlier unspecifiedn/a — no validator; codegen reads 2020-12 and draft-07 keywords+ lowSlow
TOOL-11$ref MUST NOT dereference network URIs by default2026-07-28 has26-07 ✓ only✓ — codegen resolves only local #/$defs/, #/definitions/+ medShigh
TOOL-12bound schema depth / subschemas / validation time2026-07-28 has26-07 ✓ only (SHOULD)partial — codegen depth 12 and cycle guard; no validator+ lowSlow
TOOL-13Tool.outputSchema; object-rooted until 20262025-06-18 has 2026-07-28 has opencode v1 has opencode v2 has25-06 ✓ (object) · 25-11 ✓ (object) · 26-07 ✓ (any) · opencode v1 ✓ v2 ✓ (types)partial — parsed, unused by codegen; own tools declare none+ medSmed
TOOL-14outputSchema ⇒ conforming structuredContent MUST; clients SHOULD validate2025-06-18 has opencode v1 has25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v1 enforces · v2 not tracedn/a — validates nothing; declares no outputSchema+ lowSmed
TOOL-15Tool.annotations object; untrusted unless the server is trusted2025-03-26 has24-11 — · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓partial — upstream kept raw; core ten tools carry none+ medSmed
TOOL-16readOnlyHint (default false)2025-03-26 has25-03 ✓ onward✓ — op tools set it from Mutating+ medSlow
TOOL-17destructiveHint (default true, only when not read-only)2025-03-26 has mcpx missing25-03 ✓ onward✗ — confirm gate reads absent as non-destructive (#207)+ medSmed
TOOL-18idempotentHint (default false)2025-03-26 has25-03 ✓ onward✓ — op tools set it; not used for retries+ medSmed
TOOL-19openWorldHint (default true)2025-03-26 has25-03 ✓ onwardpartial — true on every op tool, including local ones+ lowSlow
TOOL-20Tool.icons2025-11-25 has mcpx missing25-11 ✓ · 26-07 ✓✗ — dropped on parse (#207)+ lowSlow
TOOL-21Tool.execution.taskSupport; required needs a task call2025-11-25 has 2026-07-28 removes mcpx missing25-11 ✓ only · opencode v1 SDK throws on required✗ — not parsed; required upstream tools uncallable (#209)+ medMmed
TOOL-22_meta on Tool2025-06-18 has specs conflict mcpx missing25-06 ✓ · 25-11 ✓ · 26-07 ✓ (schema; prose omits)✗ — dropped on parse (#207)+ medSmed
TOOL-23tools/list in deterministic order2026-07-28 has26-07 ✓ only (SHOULD)✓ — fixed slice; upstream cache sorted by name+ medSlow
TOOL-24mcpx’s tools/list is 61 tools, not the promised tendoes not match mcpx's goalmcpx onlypartial — ten core plus 51 generated op tools+ highSmed
TOOL-25tools/list and other lists are not revision-filtered2025-06-18 has 2025-11-25 hasmcpx designpartial — safe today only because own tools carry few fields+ lowSmed
TOOL-26CallToolResult.isError; mcpx_call drops it2024-11-05 has mcpx missing opencode v1 has opencode v2 hasall five ✓ · opencode v1 ✓ v2 ✓ (thrown)✗ — upstream failures reported as success (wire W20) (#206)+ highShigh
TOOL-27CallToolResult.structuredContent; any JSON value in 20262025-06-18 has 2026-07-28 has opencode v1 has opencode v2 has25-06 ✓ (object) · 25-11 ✓ (object) · 26-07 ✓ (any)partial — downgraded below 2025-06-18; non-objects not downgraded+ medSmed
TOOL-28text duplicate of structuredContent; which one each client reads2025-06-18 has opencode v1 has opencode v2 has25-06..26-07 SHOULD · opencode v1 text first · v2 structured firstpartial — mcpx_call sends text only+ medSmed
TOOL-29mcpx_exec returns artifacts as resource_link blocks2025-06-18 has mcpx has, others don't25-06 ✓ onward · opencode v1 drops them✓ — one link per artifact+ medSmed
TOOL-30tools/call may answer InputRequiredResult; task param gone2026-07-28 has26-07 ✓ only✓ — both directions (gaps in the MRTR register)+ highSmed
TOOL-31per-call permission prompts for MCP tools in the hostopencode v1 has opencode v2 hasopencode v1 ✓ v2 ✓n/a — host-side+ lowSlow

TOOL-01 Tool.name format

  • What. 2025-11-25 (SEP-986) adds SHOULD-level naming rules: 1–128 characters, case-sensitive, only ASCII letters, digits, _, - and ., no spaces or commas, unique within a server. Earlier revisions say only “Unique identifier”.
  • Where. Rules in 2025-11-25 and 2026; all revisions require a unique name.
  • mcpx @ 05c78b2. Every name mcpx serves is mcpx_ plus lower-case words and underscores (wire W6), well inside the rules. Upstream names are not re-exposed as MCP tools; they are reached through mcpx_call {namespace, tool} and through generated TypeScript, whose identifier rules are a code-mode concern.
  • Value to mcpx. + low: nothing to change for its own names.
  • Effort. S.
  • Risk. Low. Accept liberally: these are SHOULDs, so upstream names outside them must still be callable.
  • Detail. . is allowed by the spec but not by opencode v2’s registry (TOOL-03), so a spec-conformant name like admin.tools.list is renamed by that host.
  • Sources. 2024-11-05/server/tools.mdx:183 “- name: Unique identifier for the tool”; 2025-11-25/server/tools.mdx:219 “Tool names SHOULD be between 1 and 128 characters in length (inclusive).”; 2025-11-25/server/tools.mdx:221 “The following SHOULD be the only allowed characters”; internal/mcpserver/server.go:279

TOOL-02 aggregators SHOULD disambiguate colliding names

  • What. 2026 adds a note: name uniqueness is per server; clients or proxies aggregating several servers SHOULD disambiguate (for example by prefixing a server identifier) and SHOULD NOT rely on serverInfo.name, which is not guaranteed unique.
  • Where. 2026 only. It is the only place the specification speaks directly to proxies.
  • mcpx @ 05c78b2. Namespaces come from the configuration key, sanitised, or an explicit namespace (internal/config/config.go:480), never from serverInfo.name; mcpx_call takes namespace and tool as separate arguments (wire W6), and prompts are re-exposed as <ns>_<name> (wire W19).
  • Value to mcpx. + endorses the design mcpx already has.
  • Effort. S.
  • Risk. Using serverInfo.name would collide; mcpx does not.
  • Detail. Sanitised namespaces can themselves collide (fff-nix and fff_nix); that and reserved-word identifiers are in the code-mode register.
  • Sources. 2026-07-28/server/tools.mdx:324 “Tool name uniqueness is scoped to a single server. Clients or proxies that”; 2026-07-28/server/tools.mdx:330 “servers and SHOULD NOT be relied upon for disambiguation.”; internal/config/config.go:480 “Namespace: pick(ex.Namespace, SanitizeNamespace(name)),”

TOOL-03 host-side tool names in opencode

  • What. opencode exposes each MCP tool as sanitize(server)_sanitize(tool), replacing anything outside [a-zA-Z0-9_-] with _. v2 also registers the tool under namespace server (code-mode path tools.<server>.<tool>) and rejects a normalised tool name that does not match ^[A-Za-z0-9_-]{1,64}$.
  • Where. opencode v1 and v2. The spec allows 128 characters and .; opencode v2 allows 64 and no ..
  • mcpx @ 05c78b2. mcpx’s own tools are already prefixed mcpx_ (internal/mcpserver/server.go:279), so an opencode server entry named mcpx yields mcpx_mcpx_namespaces. The longest mcpx tool name is far below 64 characters.
  • Value to mcpx. + low: the doubled prefix costs tokens on every opencode v1 request outside code mode.
  • Effort. S — document a short server key for opencode, or drop the mcpx_ prefix when served over MCP.
  • Risk. Low. v2 logs and skips an invalid registration rather than failing the server.
  • Detail. Permission keys use the same server_tool form (TOOL-31), so users can allow mcpx_* wholesale.
  • Sources. v1:packages/opencode/src/mcp/catalog.ts:119 “export const toolName = (clientName: string, name: string) => sanitize(clientName) + “; v2:packages/core/src/tool/mcp.ts:17; v2:packages/core/src/tool/mcp.ts:45; v2:packages/core/src/tool.ts:297; internal/mcpserver/server.go:279

TOOL-04 Tool.title and display precedence

  • What. 2025-06-18 gives Tool a top-level title (via BaseMetadata) and fixes display precedence: title, then annotations.title, then name.
  • Where. 2025-06-18 onward.
  • mcpx @ 05c78b2. The client keeps an upstream title (internal/mcpclient/client.go:119). mcpx’s own Tool struct has no title (internal/mcpserver/server.go:260-268), so hosts fall back to annotations.title (op tools) or the name (the ten core tools).
  • Value to mcpx. + low: human-facing listings.
  • Effort. S — but adding it needs revision filtering (TOOL-25).
  • Risk. Sending title to a 2025-03-26 host would be a send-conservatively conflict, as P-2 is for prompts.
  • Detail. The BaseMetadata.title doc singles out Tool as the one type where annotations.title outranks name.
  • Sources. schema/2025-06-18/schema.ts:930; schema/2025-11-25/schema.ts:1289 “Display name precedence order is: title, annotations.title, then name.”; internal/mcpclient/client.go:119; internal/mcpserver/server.go:260-268

TOOL-05 ToolAnnotations.title

  • What. 2025-03-26’s annotations object carries its own title, the only display name before Tool.title existed.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. Every generated op tool carries annotations.title = the operation summary (internal/api/ops.go:661); the ten core tools (mcpx_call, mcpx_exec, …) carry no annotations at all (wire W6).
  • Value to mcpx. + low: the tools a host most needs to label are the ten that lack one.
  • Effort. S.
  • Risk. Low.
  • Detail. Sending annotations to 2025-03-26 hosts is correct; it is the one tool field mcpx sends that is newer than 2024-11-05, which mcpx does not serve.
  • Sources. schema/2025-03-26/schema.ts:741 “title?: string;”; internal/api/ops.go:661 “"title": o.Summary,”

TOOL-06 Tool.description

  • What. Optional human-readable description. 2025-03-26 adds that clients can use it to improve the model’s understanding, “like a hint to the model”.
  • Where. All five; the field itself is unchanged.
  • mcpx @ 05c78b2. Always sent for mcpx’s own tools (internal/mcpserver/server.go:261, no omitempty); upstream descriptions are kept and rendered as JSDoc in generated types (code-mode register).
  • Value to mcpx. + low; the descriptions are what a host’s model pays for per tool (TOOL-24).
  • Effort. S.
  • Risk. None.
  • Detail. None beyond the wording.
  • Sources. schema/2024-11-05/schema.ts:695 “description?: string;”; schema/2025-03-26/schema.ts:793 “It can be thought of like a”; internal/mcpserver/server.go:261

TOOL-07 inputSchema shape

  • What. 2024-11-05 through 2025-11-25 type inputSchema as {type:"object", properties?, required?} (plus $schema from 2025-11-25). 2026 allows any JSON Schema 2020-12 keyword beside type (oneOf, $ref, $defs, if/then/else, …); the root must still be type: "object".
  • Where. Restricted through 2025-11-25; open in 2026.
  • mcpx @ 05c78b2. Upstream schemas are kept as raw JSON (internal/mcpclient/client.go:121); codegen understands anyOf/oneOf/allOf, prefixItems, local $ref and $defs/definitions (internal/codegen/ts.go:52-58).
  • Value to mcpx. + raw pass-through is already right; − typed codegen must keep up with composition keywords.
  • Effort. M — codegen coverage, not the wire.
  • Risk. Codegen that assumes flat properties produces wrong argument types.
  • Detail. The older TypeScript type was narrower than what servers sent in practice; 2026 aligns the schema with reality (SEP-2106). opencode v1’s SDK rejects a whole tools/list if any inputSchema.type is not the literal "object", which every revision requires anyway.
  • Sources. schema/2024-11-05/schema.ts:699 “inputSchema: {”; schema/2026-07-28/schema.ts:1997 “inputSchema: { $schema?: string; type: “; 2026-07-28/changelog.mdx:49 “Loosen inputSchema and outputSchema to allow any JSON Schema 2020-12 keywords”; internal/mcpclient/client.go:121; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/types.js L1242

TOOL-08 no-parameter tools

  • What. inputSchema MUST be a valid JSON Schema object, never null; for tools with no parameters the recommended form is {type:"object", additionalProperties:false}; {type:"object"} is also valid.
  • Where. 2025-11-25 and 2026.
  • mcpx @ 05c78b2. Every no-argument mcpx tool uses exactly the recommended form (internal/mcpserver/server.go:283; wire W6).
  • Value to mcpx. + already conforms.
  • Effort. None.
  • Risk. Upstreams sending inputSchema: null should still be tolerated when read (accept liberally).
  • Detail. Guidance only.
  • Sources. 2025-11-25/server/tools.mdx:201 “MUST be a valid JSON Schema object (not null)”; 2025-11-25/server/tools.mdx:203; internal/mcpserver/server.go:283

TOOL-09 x-mcp-header parameter annotation

  • What. A property in inputSchema may carry x-mcp-header: "<Name>"; a Streamable-HTTP client MUST mirror that argument into an Mcp-Param-<Name> header so intermediaries can route without parsing the body. Clients on HTTP MUST reject (drop from their tools/list view) any tool whose annotation breaks the constraints, and SHOULD log a warning.
  • Where. 2026 only. stdio clients MAY ignore it.
  • mcpx @ 05c78b2. Neither x-mcp-header nor Mcp-Param appears in internal/: annotated tools are neither validated nor mirrored when mcpx calls an HTTP upstream, and invalid ones are not filtered from mcpx’s catalogue.
  • Value to mcpx. + mcpx is an HTTP client of remote upstreams, where this is mandatory.
  • Effort. M — validation plus header encoding (transports register).
  • Risk. If not done: 2026 HTTP servers behind header-routing intermediaries reject or misroute mcpx’s calls, and mcpx exposes tool definitions it was required to drop.
  • Detail. Constraints on the value: non-empty; HTTP token syntax; no CR/LF; unique case-insensitively within the schema; only on integer, string or boolean properties (not number), integers within ±(2^53−1); only on properties statically reachable from the root. Servers SHOULD NOT mark secrets. A null/absent argument omits the header.
  • Also recorded from the transports register. Not implemented; such tools would be listed in mcpx’s catalog and callable through mcpx_call.
  • Sources. 2026-07-28/server/tools.mdx:362 “Clients using the Streamable HTTP transport MUST reject tool definitions where any”; 2026-07-28/server/tools.mdx:346-359; 2026-07-28/basic/transports/streamable-http.mdx:364 “While the use of x-mcp-header is optional for servers, clients MUST”; 2026-07-28/basic/transports/streamable-http.mdx:408; 2026-07-28/basic/transports/streamable-http.mdx:402 “Clients using the Streamable HTTP transport MUST reject tool definitions”

TOOL-10 JSON Schema 2020-12 as the default dialect

  • What. Schemas without $schema are JSON Schema 2020-12; implementations MUST support 2020-12 and MUST handle unsupported dialects gracefully.
  • Where. 2025-11-25 and 2026; earlier revisions leave the dialect unspecified.
  • mcpx @ 05c78b2. mcpx has no JSON Schema validator (go.mod has no schema library). Codegen reads both 2020-12 keywords ($defs, prefixItems) and draft-07 definitions (internal/codegen/ts.go:52-58).
  • Value to mcpx. + low: matters only if mcpx starts validating arguments or results.
  • Effort. S–M, depending on the validator chosen.
  • Risk. A validator defaulting to draft-07 would misread $defs, prefixItems and friends.
  • Detail. Applies to inputSchema, outputSchema and elicitation requestedSchema.
  • Sources. 2025-11-25/basic/index.mdx:144 “Default dialect: When a schema does not include a $schema field, it defaults to”; 2025-11-25/changelog.mdx:33 “Establish JSON Schema 2020-12 as the default dialect”; internal/codegen/ts.go:52-58

TOOL-11 $ref network dereference forbidden by default

  • What. Implementations MUST NOT automatically dereference $refs that resolve to a network URI. An opt-in fetch mode MUST be off by default and SHOULD allowlist hosts, reject loopback/link-local/private addresses, apply timeouts and size limits, and log fetched URIs. A schema that fails on an unresolved external $ref SHOULD be rejected, not treated as permissive.
  • Where. 2026 only.
  • mcpx @ 05c78b2. Codegen resolves only #/$defs/ and #/definitions/ references (internal/codegen/ts.go:263-276) and renders anything else as unknown (internal/codegen/ts.go:119); nothing fetches.
  • Value to mcpx. + keep it that way: a daemon on loopback that ingests untrusted upstream schemas is the SSRF case the rule exists for.
  • Effort. S (none today).
  • Risk. High if a future validator is added with default network loading.
  • Detail. Rendering an unresolved external ref as unknown is permissive for types; that is codegen, not validation, so the SHOULD about rejecting such schemas does not bite yet.
  • Sources. 2026-07-28/basic/index.mdx:301 “JSON Schema 2020-12 permits $ref to point at an absolute URI. Implementations MUST NOT”; 2026-07-28/server/tools.mdx:800; internal/codegen/ts.go:263-276

TOOL-12 bounds on composition keywords

  • What. Implementations SHOULD bound schema depth, the number of subschemas, or validation time so a hostile schema cannot stall a validator.
  • Where. 2026 only; SHOULD, no numbers given.
  • mcpx @ 05c78b2. No validator. Codegen stops at depth 12 and breaks $ref cycles (internal/codegen/ts.go:103-104, internal/codegen/ts.go:111-112).
  • Value to mcpx. + low: a pooled daemon reading many upstream schemas benefits if it ever validates.
  • Effort. S–M.
  • Risk. Low today.
  • Detail. The depth 12 is an inline constant, against mcpx’s “nothing hardcoded” rule.
  • Sources. 2026-07-28/basic/index.mdx:315 “expressive schemas but can be expensive to validate. Implementations SHOULD apply”; internal/codegen/ts.go:103 “if r.depth > 12 {”

TOOL-13 Tool.outputSchema

  • What. Optional JSON Schema for structuredContent. 2025-06-18 and 2025-11-25 require type: "object" at the root; 2026 allows any 2020-12 schema (arrays, scalars).
  • Where. 2025-06-18 onward; loosened in 2026. opencode v1 and v2 feed it into their code-mode type signatures.
  • mcpx @ 05c78b2. The client parses it (internal/mcpclient/client.go:122), but codegen’s Tool has no such field (internal/codegen/ts.go:19-23) and every generated function returns ToolResult = any (internal/codegen/emit.go:458). mcpx’s own tools declare none.
  • Value to mcpx. + typed results for mcpx exec scripts; codegen already unwraps structuredContent.
  • Effort. S to type results; M to validate.
  • Risk. If mcpx ever re-exposes upstream definitions over MCP, a 2026 upstream’s array-rooted outputSchema is invalid for 2025-06-18/2025-11-25 hosts — a downgrade case downgrade() does not cover.
  • Detail. The code-mode register has the typing comparison (opencode types results from it; lootbox and mcpx do not).
  • Sources. schema/2025-06-18/schema.ts:951 “outputSchema?: {”; schema/2025-11-25/schema.ts:1277 “Currently restricted to type: “; schema/2026-07-28/schema.ts:2005 “outputSchema?: { $schema?: string; [key: string]: unknown };”; internal/mcpclient/client.go:122; internal/codegen/emit.go:458; v1:packages/opencode/src/tool/code-mode.ts:127; v2:packages/core/src/tool/mcp.ts:48

TOOL-14 outputSchema validation obligations

  • What. If a tool declares outputSchema, servers MUST return conforming structuredContent; clients SHOULD validate it.
  • Where. 2025-06-18 onward. opencode v1’s SDK enforces it: a call to a tool with outputSchema that returns no structuredContent (and is not isError) throws, and mismatching content throws InvalidParams; if listing fails on an unresolvable $ref in some outputSchema, v1 re-lists with outputSchema stripped. v2 was not traced.
  • mcpx @ 05c78b2. mcpx validates nothing upstream and declares no outputSchema on its own tools, so opencode v1’s enforcement never trips on mcpx.
  • Value to mcpx. + low today; a constraint on the future.
  • Effort. S.
  • Risk. If mcpx adds outputSchema to mcpx_call or mcpx_exec, every success path must emit structuredContent, or opencode v1 fails the call.
  • Detail. v2 stops JSON-parsing text results once a tool declares outputSchema (TOOL-28).
  • Sources. 2025-06-18/server/tools.mdx:314 “Servers MUST provide structured results that conform to this schema.”; 2025-06-18/server/tools.mdx:315 “Clients SHOULD validate structured results against this schema.”; v1:packages/opencode/src/mcp/catalog.ts:14; v1:packages/opencode/src/mcp/catalog.ts:155; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/client/index.js L500–L501

TOOL-15 Tool.annotations

  • What. Optional behaviour hints on each tool (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Clients MUST treat them as untrusted unless they come from trusted servers.
  • Where. 2025-03-26 onward; absent in 2024-11-05.
  • mcpx @ 05c78b2. Upstream annotations are kept raw (internal/mcpclient/client.go:128). mcpx’s generated op tools carry all five fields (internal/api/ops.go:659-667); the ten core tools carry none (wire W6), so by the defaults a host must assume mcpx_call and mcpx_exec are destructive, non-idempotent and open-world.
  • Value to mcpx. + hosts drive confirm-before-run from these; mcpx_namespaces or mcpx_types marked read-only could be auto-approved.
  • Effort. S.
  • Risk. Trusting hints from an untrusted upstream; or, for mcpx’s own tools, hosts asking more than they need to.
  • Detail. For mcpx_call and mcpx_exec the honest answer depends on the upstream tool, so leaving them unannotated (defaults) is defensible; the discovery tools are unambiguously read-only.
  • Sources. schema/2025-03-26/schema.ts:809 “annotations?: ToolAnnotations;”; 2025-03-26/server/tools.mdx:189 “tool annotations to be untrusted unless they come from trusted servers.”; internal/mcpclient/client.go:128; internal/api/ops.go:659-667

TOOL-16 readOnlyHint

  • What. true means the tool does not modify its environment. Default false: absent is not read-only.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. Op tools set readOnlyHint: !o.Mutating (internal/api/ops.go:662); mcpx does not read it on upstream tools.
  • Value to mcpx. + the primary auto-approve signal for hosts.
  • Effort. S.
  • Risk. Low.
  • Detail. destructiveHint and idempotentHint are meaningful only when readOnlyHint is false.
  • Sources. schema/2025-03-26/schema.ts:748 “readOnlyHint?: boolean;”; schema/2026-07-28/schema.ts:1921 “Default: false”; internal/api/ops.go:662 “"readOnlyHint": !o.Mutating,”

TOOL-17 destructiveHint

  • What. true means the tool may perform destructive updates; false means additive only. Meaningful only when readOnlyHint is false. Default true.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. mcpx acts on it: with elicit.confirmDestructive on (default off), a call to an upstream tool annotated destructive asks through the broker first (internal/settings/consumer.go:47-49). But the check returns false when annotations or the field are absent (internal/daemon/consumer.go:414-425) — the opposite of the spec default — and ignores readOnlyHint. Op tools set it from o.Destructive (internal/api/ops.go:663).
  • Value to mcpx. + the confirmation gate is a real safety feature for scripts; it should use the spec default.
  • Effort. S — absent destructiveHint with readOnlyHint not true counts as destructive.
  • Risk. If not fixed: an unannotated upstream tool that deletes things runs unconfirmed with the setting on. If fixed: more confirmations for unannotated tools, which is what the default means.
  • Detail. Reading absent as non-destructive is exactly the misreading the spec default exists to prevent.
  • Sources. schema/2025-03-26/schema.ts:758 “destructiveHint?: boolean;”; schema/2025-03-26/schema.ts:756 “Default: true”; internal/daemon/consumer.go:414-425; internal/settings/consumer.go:49 “ask before a call to a tool annotated destructiveHint”

TOOL-18 idempotentHint

  • What. true means repeating a call with the same arguments has no further effect. Meaningful only when not read-only. Default false.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. Op tools set idempotentHint: !o.Mutating || o.Destructive (internal/api/ops.go:664). mcpx does not consult it before retrying anything.
  • Value to mcpx. + 2026 removed stream resumability, so after a dropped 2026 stream the only safe-retry signal the protocol offers is this hint.
  • Effort. S.
  • Risk. Retrying a non-idempotent tool after a dropped stream double-executes.
  • Detail. 2026: a broken stream means the request is lost and clients MUST re-issue it as a new request.
  • Sources. schema/2025-03-26/schema.ts:768 “idempotentHint?: boolean;”; 2026-07-28/changelog.mdx:28 “clients MUST re-issue it as a new request with a new request ID”; internal/api/ops.go:664

TOOL-19 openWorldHint

  • What. true means the tool interacts with an open world of external entities (web search, say); false a closed domain. Default true.
  • Where. 2025-03-26 onward.
  • mcpx @ 05c78b2. Every op tool declares openWorldHint: true (internal/api/ops.go:665), including ones that only read the local daemon (mcpx_health, mcpx_settings_get; wire W6).
  • Value to mcpx. + low: false for daemon-local operations is the accurate answer.
  • Effort. S.
  • Risk. Low — hosts may be more cautious than needed.
  • Detail. Operations that reach a registry or an upstream (mcpx_registry_search, mcpx_resource_read) are genuinely open-world.
  • Sources. schema/2025-03-26/schema.ts:778 “openWorldHint?: boolean;”; schema/2025-03-26/schema.ts:776 “Default: true”; internal/api/ops.go:665 “"openWorldHint": true,”

TOOL-20 Tool.icons

  • What. Tool extends BaseMetadata, Icons: optional icons: Icon[] with src, mimeType, sizes, theme.
  • Where. 2025-11-25 and 2026.
  • mcpx @ 05c78b2. The client Tool struct has no icons field, so they are dropped on parse (internal/mcpclient/client.go:117-129).
  • Value to mcpx. + plugin/TUI display.
  • Effort. S.
  • Risk. Loss of display data only.
  • Detail. Clients that render icons MUST support PNG and JPEG and SHOULD support SVG and WebP; SVG can carry script.
  • Sources. schema/2025-11-25/schema.ts:1249 “export interface Tool extends BaseMetadata, Icons {”; 2025-11-25/changelog.mdx:13 “Allow servers to expose icons as additional metadata for tools, resources, resource templates, and prompts”; internal/mcpclient/client.go:117-129

TOOL-21 Tool.execution.taskSupport

  • What. Per-tool task negotiation: "forbidden" (default), "optional", "required". For required, clients MUST call the tool as a task or get -32601. The server must also declare tasks.requests.tools.call.
  • Where. 2025-11-25 only; removed from the 2026 schema (the tasks extension has no per-tool negotiation), which the 2026 changelog does not mention. opencode v1’s SDK throws InvalidRequest when asked to call a tool cached as task-required; neither opencode version declares client tasks.
  • mcpx @ 05c78b2. Not parsed (internal/mcpclient/client.go:117-129); CallTool sends only {name, arguments} (internal/mcpclient/client.go:845-858) and no tasks/* method exists in the client or pool. A 2025-11-25 upstream tool with taskSupport: "required" answers -32601 and mcpx has no way around it. mcpx’s own tools carry no execution (internal/mcpserver/server.go:260-268), which makes its declared tasks.requests.tools.call unreachable (capabilities register).
  • Value to mcpx. + long-running upstream jobs (CI, batch) survive disconnects; mcpx already has a task store to bridge them.
  • Effort. M.
  • Risk. If not done: a growing class of servers is uncallable through mcpx. If mcpx ever marks its own tool required, opencode v1 cannot call it.
  • Detail. docs/protocol.md says upstream task status notifications are dropped because “mcpx polls task state”; mcpx polls nothing upstream (tasks register).
  • Also recorded from the tasks register. 2025-11-25. opencode v1’s SDK throws InvalidRequest before sending a call to a tool cached as task-required; neither opencode version can run tasks. The client sends tools/call with {name, arguments} only (internal/mcpclient/client.go:845-858), never params.task, and handles no CreateTaskResult; no tasks/* method exists in internal/mcpclient or internal/pool. Such a tool answers -32601 and mcpx has no way around it. In 2026 the equivalent is declaring the extension per request and accepting server-chosen tasks, which mcpx’s client does not do either. docs/protocol.md:297-299 claims mcpx “polls task state”; with no upstream tasks there is nothing to poll (TASK-14).
  • Sources. schema/2025-11-25/schema.ts:1241 “taskSupport?: “; 2025-11-25/basic/utilities/tasks.mdx:117 “If execution.taskSupport is "; internal/mcpclient/client.go:849; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/client/index.js L490–L494; v1:packages/opencode/src/mcp/index.ts:48` “// tasks: {},”; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/client/index.js (L490–L494).

TOOL-22 _meta on Tool

  • What. 2025-06-18 adds _meta to Tool (and to resources, templates, prompts and content blocks). Keys whose second label is modelcontextprotocol or mcp are reserved.
  • Where. 2025-06-18 onward. The 2026 schema has it; the 2026 Tool data-type prose list omits it.
  • mcpx @ 05c78b2. The client Tool struct has no _meta, so it is dropped on parse (internal/mcpclient/client.go:117-129).
  • Value to mcpx. + MCP Apps and other extensions hang per-tool data here; a proxy that drops it breaks them.
  • Effort. S — keep it raw, like annotations.
  • Risk. Extension metadata silently lost.
  • Detail. The prose/schema gap in 2026 is an omission, not a removal: the schema is authoritative. Upstream result _meta loss is in the _meta register.
  • Sources. schema/2025-06-18/schema.ts:967 “_meta?: { [key: string]: unknown };”; 2025-06-18/changelog.mdx:35 “Add _meta field to additional interface types”; schema/2026-07-28/schema.ts:2014; 2026-07-28/server/tools.mdx:286-302; internal/mcpclient/client.go:117-129

TOOL-23 deterministic tools/list order

  • What. Servers SHOULD return tools in the same order across requests while the set is unchanged, for client caching and LLM prompt-cache hits.
  • Where. 2026 only; SHOULD; tools/list alone (not prompts or resources).
  • mcpx @ 05c78b2. The ten core tools come first, then adapter, OpenAPI and op tools in declaration order (internal/mcpserver/server.go:376-378; internal/cli/serve.go:332-347). The upstream schema cache is sorted by name (internal/pool/pool.go:501-503).
  • Value to mcpx. + prompt-cache hits for every host.
  • Effort. S (done).
  • Risk. Go map iteration would randomise a listing built from a map; none is today.
  • Detail. Merges the tools and caching findings on the same rule; the pagination-caching register refers here.
  • Sources. 2026-07-28/server/tools.mdx:71 “Servers SHOULD return tools in a deterministic order”; 2026-07-28/changelog.mdx:34 “Servers SHOULD return tools from tools/list in a deterministic order”; internal/mcpserver/server.go:376-378; internal/pool/pool.go:501-503

TOOL-24 mcpx’s tools/list is 61 tools

  • What. Besides the ten core tools, every generated /v1 operation is exposed as its own MCP tool — 61 in total with one upstream configured. The package doc and comments say “Ten tools instead of three hundred”.
  • Where. mcpx only; the aim is to keep upstream schemas out of the host’s context.
  • mcpx @ 05c78b2. Base list internal/mcpserver/server.go:276-380; op tools from api.Ops() (internal/mcpserver/ops.go:30-60), appended in internal/cli/serve.go:344. Wire W6 returned 61 tools.
  • Value to mcpx. − about 51 extra schemas in every host’s context, the cost mcpx exists to avoid; + the parity principle (CLI = /v1 = MCP) is why they are there.
  • Effort. S — gate op tools behind a setting, or expose one dispatcher tool.
  • Risk. Token cost on every request of every host that sends tool schemas to its model directly.
  • Detail. A product decision between two stated principles; the “ten” comment is stale either way.
  • Sources. internal/mcpserver/server.go:12-13 “Ten tools”; internal/mcpserver/server.go:274-275 “These ten reach”; internal/cli/serve.go:344 “extras = append(extras, a.v1OpTools()…)”; internal/mcpserver/ops.go:30-60

TOOL-25 lists are not revision-filtered

  • What. mcpx builds every result in the newest shape and spells it down in downgrade(), but downgrade() rewrites only content blocks, prompt messages, structuredContent and resultType; it never touches tools/list or any other list result.
  • Where. mcpx design. Tool title/outputSchema/_meta are 2025-06-18 fields; icons/execution are 2025-11-25.
  • mcpx @ 05c78b2. mcpx’s tools carry only name, description, inputSchema and annotations (wire W6 key set; internal/mcpserver/server.go:260-268), all defined in 2025-03-26, so nothing leaks today. internal/mcpserver/server.go:439 applies downgrade to every result; internal/mcpserver/revisions.go:132-170 shows what it handles.
  • Value to mcpx. + low now; + necessary before adding title, outputSchema, icons or execution.
  • Effort. S.
  • Risk. The first newer field added to a tool leaks to older hosts; prompts already leak title to 2025-03-26 (prompts register).
  • Detail. downgrade() copies before editing, so stored task results are not mutated.
  • Sources. internal/mcpserver/server.go:439 “resp.Result = downgrade(resp.Result, peer.Version)”; internal/mcpserver/revisions.go:132-170; internal/mcpserver/server.go:260-268

TOOL-26 CallToolResult.isError, and mcpx_call dropping it

  • What. Tool execution failures are reported in the result with isError: true, not as protocol errors, so the model can see and react to them.
  • Where. All five. opencode v1 throws an error built from the text blocks (or “MCP tool returned an error”) and discards non-text blocks; v2 raises a ToolFailure the same way. lootbox hands the envelope to the script as a successful value (code-mode register).
  • mcpx @ 05c78b2. mcpx’s own failures are correct: a failing tool becomes isError: true (internal/mcpserver/server.go:666-674). But mcpx_call launders upstream failures: /v1/call returns 200 with the raw result (internal/daemon/server.go:694-703), mcpBackend.Call returns renderResult(res.Result), nil (internal/cli/serve.go:103-117), and renderResult parses IsError and ignores it (internal/cli/commands.go:385-415). Wire W20: upstream boom with isError: true came back as {"content":[{"text":"boom: deliberate failure","type":"text"}],"resultType":"complete"}.
  • Value to mcpx. + correctness of the core proxy tool.
  • Effort. S — return an error from mcpBackend.Call when the upstream result has isError.
  • Risk. If not fixed: agents act on error text as if it were data.
  • Detail. Scripts are unaffected: the generated client throws ToolError on isError (internal/codegen/emit.go:493-500). A task created for such a call is marked failed only if mcpx’s own result has isError, which here it never does. The ask path sets isError only for transport failures (internal/cli/serve_ask.go:175-181). Whether bad arguments are isError or -32602 is in the errors register.
  • Also recorded from the content types register. isError exists in every revision. opencode v1 and v2 would turn an isError result into a thrown error carrying the joined text blocks (non-text blocks of an error result are discarded), if mcpx passed it on. /v1/call returns 200 with the raw result (internal/daemon/server.go:694-703); renderResult parses IsError and ignores it (internal/cli/commands.go:392); the ask path sets IsError only for transport failures (internal/cli/serve_ask.go:175-181). Wire W20: fakemcp’s boom came back as {"content":[{"text":"boom: deliberate failure","type":"text"}],"resultType":"complete"}. Scripts are unaffected: the generated client throws ToolError (CT-19). The errors area records the protocol-error-vs-isError rules this sits on.
  • Sources. schema/2024-11-05/schema.ts:663 “isError?: boolean;”; internal/cli/commands.go:392; internal/cli/serve.go:116 “return renderResult(res.Result), nil”; internal/mcpserver/server.go:666-674; v1:packages/opencode/src/mcp/catalog.ts:68; v2:packages/core/src/tool/mcp.ts:78; wire W20

TOOL-27 CallToolResult.structuredContent

  • What. 2025-06-18 adds structuredContent: {[key]: unknown} (an object). 2026 retypes it unknown: any JSON value, including arrays, scalars and null (distinct from absent).
  • Where. Absent in 2024-11-05 and 2025-03-26; object in 2025-06-18 and 2025-11-25; any JSON in 2026. The same change applies to sampling’s ToolResultContent.structuredContent.
  • mcpx @ 05c78b2. For hosts below 2025-06-18, downgrade() removes it and appends it as an indented JSON text block (internal/mcpserver/revisions.go:146-156). There is no downgrade of a non-object value for 2025-06-18/2025-11-25 hosts. In practice mcpx never emits structuredContent (CT-13), so the branch is dead today.
  • Value to mcpx. + needed as soon as mcpx relays structured results.
  • Effort. S — wrap or textify non-object values for 2025-06-18/2025-11-25 hosts.
  • Risk. A 2026 upstream returning [...] or 42 relayed verbatim is schema-invalid for 2025-06-18/2025-11-25 hosts.
  • Detail. opencode v1 treats structuredContent: null like absent (TOOL-28), which erases the 2026 distinction.
  • Also recorded from the content types register. 2025-06-18 onward. opencode v1’s direct path uses it only when content is empty (then as JSON.stringify text); v1 code mode prefers it over text; v2 makes it the tool’s output whenever present. Below 2025-06-18, downgrade() deletes it and appends it as an indented JSON text block (internal/mcpserver/revisions.go:146-156). For 2025-06-18 and 2025-11-25 clients a non-object value from a 2026 upstream is passed unchanged, which their schema forbids. mcpx itself emits no structuredContent through mcpx_call (CT-13), so the branch is dead in production. null is a legal 2026 value distinct from absent. opencode v1’s SDK also enforces “tool has outputSchema ⇒ result has structuredContent” (tools area).
  • Sources. schema/2025-06-18/schema.ts:828 “structuredContent?: { [key: string]: unknown };”; schema/2026-07-28/schema.ts:1821 “structuredContent?: unknown;”; schema/2025-11-25/schema.ts:1891; schema/2026-07-28/schema.ts:2469; internal/mcpserver/revisions.go:146-156; 2026-07-28/server/tools.mdx:500 “For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.”; internal/mcpserver/revisions.go:200-203; v1:packages/opencode/src/mcp/catalog.ts:75; v1:packages/opencode/src/tool/code-mode.ts:109; v2:packages/core/src/tool/mcp.ts:97

TOOL-28 structuredContent versus its text duplicate

  • What. For backwards compatibility a tool returning structuredContent SHOULD also return the serialized JSON in a text block. Clients then pick one.
  • Where. 2025-06-18 onward. opencode v1 returns content as-is when non-empty and uses structuredContent only when content is empty (and not null); v2 makes structuredContent the tool’s output, else parses text that starts with {/[ as JSON when the tool has no outputSchema, else passes text.
  • mcpx @ 05c78b2. mcpx_call sends one text block and never structuredContent (CT-13), so v2 code mode gets data only through its JSON-looking-text heuristic. Generated script clients return structuredContent when present (internal/codegen/emit.go:501).
  • Value to mcpx. + returning upstream structuredContent alongside the text would give v2 real values and keep v1 working.
  • Effort. S.
  • Risk. Without the text duplicate, opencode v1 shows the model nothing useful; without structuredContent, v2 guesses.
  • Detail. 2026 prose adds that structuredContent is unrelated to LLM “structured outputs”.
  • Sources. 2026-07-28/server/tools.mdx:500 “For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.”; v1:packages/opencode/src/mcp/catalog.ts:75; v2:packages/core/src/tool/mcp.ts:96; v2:packages/core/src/tool/mcp.ts:100
  • What. Files a script produces come back as resource_link blocks (uri mcpx://artifacts/{id}, name, mimeType, description), read later with resources/read if the host wants the bytes.
  • Where. resource_link exists from 2025-06-18. Neither opencode nor lootbox returns artifacts this way; opencode v1 silently drops resource_link from direct tool results, and v2 keeps only the uri as text.
  • mcpx @ 05c78b2. Built in internal/cli/serve.go:174-187, carried through the string result with a U+001E marker (internal/mcpserver/toolresult.go:21), appended in tools/call (internal/mcpserver/server.go:676-681); for 2025-03-26 hosts it becomes an embedded resource (content-types register).
  • Value to mcpx. + keeps binaries out of context.
  • Effort. S (done); + S to also name artifact URIs in the text block so opencode v1 users see them.
  • Risk. Artifacts appear to vanish in opencode v1 outside code mode.
  • Detail. description on a resource_link is valid (it extends Resource).
  • Sources. internal/cli/serve.go:180 “"type": "resource_link",”; internal/mcpserver/toolresult.go:21; internal/mcpserver/server.go:676-681; v1:packages/opencode/src/session/tools.ts:429; v2:packages/core/src/mcp/client.ts:327

TOOL-30 tools/call may answer with InputRequiredResult

  • What. In 2026, CallToolResultResponse.result is CallToolResult | InputRequiredResult, and CallToolRequestParams extends InputResponseRequestParams (inputResponses, requestState) instead of TaskAugmentedRequestParams, so the 2025-11-25 task field is gone.
  • Where. 2026 only.
  • mcpx @ 05c78b2. As a client it resolves input_required and retries (internal/mcpclient/client.go:654-677); as a server it emits it for tools/call (internal/mcpserver/server.go:649-657). Its gaps (session dependence, text-only final render, no method binding in requestState) are in the MRTR register.
  • Value to mcpx. + implemented; it is how a 2026 upstream asks a question mid-call.
  • Effort. Done.
  • Risk. See the MRTR register.
  • Detail. mcpx still accepts a task param on tools/call from any era (tasks register).
  • Sources. schema/2026-07-28/schema.ts:1849 “result: CallToolResult | InputRequiredResult;”; schema/2026-07-28/schema.ts:1863 “export interface CallToolRequestParams extends InputResponseRequestParams {”; internal/mcpclient/client.go:654-677; internal/mcpserver/server.go:649-657

TOOL-31 per-call permission prompts in the host

  • What. Hosts gate each MCP tool call on a permission: opencode v1 asks with permission: "<server>_<tool>" and patterns ["*"]; v2 calls permission.assert({action: "<server>_<tool>", resources: ["*"]}).
  • Where. opencode v1 and v2. v1 code-mode child calls ask per child tool too.
  • mcpx @ 05c78b2. n/a — host-side. Every upstream call made inside one mcpx_exec is a single permission from the host’s point of view.
  • Value to mcpx. + users can allow mcpx_* wholesale; − one approval then covers everything a script does.
  • Effort. S (documentation).
  • Risk. Low.
  • Detail. The permission key is the doubled name from TOOL-03 when the server entry is called mcpx.
  • Sources. v1:packages/opencode/src/session/tools.ts:408; v2:packages/core/src/tool/mcp.ts:51; v1:packages/opencode/src/tool/code-mode.ts:147

Transports

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment:    1
status:       standard
tags:         area:compare
description:  stdio, HTTP+SSE and Streamable HTTP across revisions: framing, streams, sessions, headers, Origin, and mcpx's server and client sides.

MCP runs over stdio (newline-delimited JSON-RPC) and over HTTP: the two-endpoint HTTP+SSE transport of 2024-11-05, Streamable HTTP from 2025-03-26 with optional Mcp-Session-Id sessions and resumable SSE, and the 2026-07-28 reshape that is POST-only, sessionless, and mirrors body fields into required headers (Mcp-Method, Mcp-Name, Mcp-Param-*). For mcpx two things matter most: its /mcp endpoint runs tools as the user on a loopback port and does not validate Origin, the one MUST in this area with a working browser exploit; and its HTTP client sends none of the 2026 headers, so a conformant 2026 HTTP server rejects every modern request from it.

Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.

IDDifferenceLabelsWhere it existsmcpx @ 05c78b2ValueEffortRisk
TR-01stdio framing: one message per line, stdout only MCP2024-11-05 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — both sides; client skips non-JSON banner lines+ highSlow
TR-02stdio stderr: logging only, not an error signal2025-11-25 has opencode v1 has opencode v2 has24-11 "logging" · 25-11 any logging, not errors · 26-07 same · opencode v1 piped unread · v2 drained✓ — server silent; client drains to a ring buffer+ lowSlow
TR-03Maximum stdio frame sizeopencode v2 hasspec: none · opencode v2 16 MiB · mcpx 64 MiB✓ — 64 MiB both sides; larger than opencode v2 accepts+ medSmed
TR-04stdio server launch: environment and working directoryopencode v1 has opencode v2 hasopencode v1 ✓ · opencode v2 ✓ · mcpx ✓✓ — config env and cwd over the daemon’s env+ lowSlow
TR-05stdio requests handled one at a timemcpx missingnone required; cancellation and ping assume concurrencypartial — only calls that may ask the client run concurrently (#202)+ medMmed
TR-062026 stdio direction rules2026-07-28 has26-07 ✓✓ — legacy gets requests, modern gets input_required+ medSlow
TR-07Custom byte-stream transports reuse stdio framing2026-07-28 has mcpx has, others don't26-07 SHOULDpartial — MCP over HTTP on a Unix socket, not NDJSON+ lowMlow
TR-08JSON-RPC messages MUST be UTF-82025-03-26 has25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — Go encoding/json emits UTF-8+ lowSlow
TR-09JSON-RPC batching2025-03-26 has 2025-06-18 removes mcpx missing25-03 ✓ only✗ — server rejects batches; client splits HTTP, drops stdio (#202)+ lowMlow
TR-10HTTP+SSE transport (2024-11-05)2024-11-05 has 2025-03-26 deprecates 2026-07-28 deprecates mcpx missing opencode v1 has24-11 ✓ · 25-03 dep · 25-06 dep · 25-11 dep · 26-07 dep · opencode v1 fallback✗ — neither served nor spoken; transport: sse ignored (#220)+ medMmed
TR-11Client fallback from Streamable HTTP to HTTP+SSEspecs conflict mcpx missing opencode v1 has25-03 any 4xx · 25-11 400/404/405 · 26-07 + body check✗ — no fallback (#220)+ medMmed
TR-12Streamable HTTP methods: POST+GET → POST only2025-03-26 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 POST✓ — POST served, GET 405, DELETE 204+ lowSlow
TR-13Standalone GET SSE stream: server side2025-03-26 has 2026-07-28 removes has better replacement25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 405✓ — 405 with Allow: POST, DELETE+ lowSlow
TR-14Standalone GET SSE stream: client side2025-03-26 has mcpx missing25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✗ — never opened; unsolicited legacy messages lost (#203)+ medMmed
TR-15Accept: application/json, text/event-stream on POST2025-03-26 has25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — client sends it; server does not require it+ lowSlow
TR-16Request answered with JSON or SSE, chosen per request2025-03-26 has25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — JSON unless mcpx must ask the client something+ medSlow
TR-17202 Accepted for notifications (and legacy responses)2025-03-26 has 2026-07-28 has25-03..25-11 notifications and responses · 26-07 notifications onlypartial — known notifications 202; unknown ones get a 200 error+ lowSlow
TR-18Client responses POSTed back to the server2025-03-26 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 forbidden✓ — routed by session to the waiting request+ medSlow
TR-19Server requests on a request’s SSE stream2025-03-26 has 2026-07-28 removes has better replacement25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 forbidden✓ — legacy elicitation there; modern gets input_required+ medSlow
TR-20Each message on exactly one stream (no broadcast)2025-03-26 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 per-request streams✓ — only per-request streams exist+ lowSlow
TR-21SSE resumability via event ids and Last-Event-ID2025-03-26 has 2025-11-25 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓+ · 26-07 —✗ — no event ids sent; no resume attempted+ lowMlow
TR-22SSE polling: priming event, retry, reconnect2025-11-25 has 2026-07-28 removes mcpx missing25-11 ✓ only✗ — client ignores retry; never reconnects (#203)+ lowMmed
TR-23X-Accel-Buffering: no on SSE responses2026-07-28 has26-07 SHOULD✓ — set on every SSE response+ lowSlow
TR-24SSE comment lines as keep-alive2026-07-28 has26-07 encouraged✓ — client ignores comments; server sends none+ lowSlow
TR-25Mcp-Session-Id value and assignment rules2025-03-26 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 —✓ — sess- + 32 random hex; never required+ lowSlow
TR-26Mcp-Session-Id minted on server/discover2026-07-28 removes mcpx missing26-07: do not mint or echo✗ — minted for every discover, modern or not (#199)+ medMmed
TR-27Mcp-Session-Id minted on a failed initialize2025-03-26 has mcpx missing25-03 ✓ · 25-06 ✓ · 25-11 ✓✗ — issued even when the handshake is refused (#202)+ lowSlow
TR-28Unknown or expired session id → 4042025-03-26 has mcpx missing25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ignore✗ — served statelessly as 2025-03-26 (#202)+ medSmed
TR-29Client re-initializes after a session 4042025-03-26 has opencode v1 has opencode v2 has mcpx has25-03 ✓ · 25-06 ✓ · 25-11 ✓ · opencode v1 ✓ · opencode v2 ✓✓ — client forgets the dead session, re-initializes once and retries once (#1637)+ medMlow
TR-30Client sends a session id to modern servers2026-07-28 removes mcpx missing26-07: no sessions✗ — captured from any response, echoed always (#200)+ lowSlow
TR-31DELETE handled by the server2025-03-26 has 2026-07-28 removes25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 405✓ — 204, even for an unknown session+ lowSlow
TR-32DELETE sent by the client on close2025-03-26 has25-03 ✓ · 25-06 ✓ · 25-11 ✓✓ — sent when a session id is held+ lowSlow
TR-33MCP-Protocol-Version header requirement timeline2025-06-18 has specs conflict25-03 auth only · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (= _meta)partial — sent always; server checks only against _meta+ medSmed
TR-34Client header value after a legacy handshake2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓✗ — always 2025-11-25, not the negotiated version (#203)+ medSmed
TR-35Server uses the legacy header as the version2025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓partial — header ignored; no-session requests served as 2025-03-26 (#202)+ medSmed
TR-36Invalid legacy header value → 4002025-06-18 has mcpx missing25-06 ✓ · 25-11 ✓✗ — 1999-01-01 accepted and served (#202)+ lowSlow
TR-37Mcp-Method header (2026)2026-07-28 has mcpx missing26-07 REQUIRED✗ — client never sends it; server never checks it (#199)+ highShigh
TR-38Mcp-Name header (2026)2026-07-28 has mcpx missing26-07 REQUIRED on three methods✗ — client never sends it; server never checks it (#199)+ highShigh
TR-39Mirror x-mcp-header arguments into Mcp-Param-{Name}2026-07-28 has mcpx missing26-07 clients MUST✗ — not implemented (#200)+ medMmed
TR-40Intermediaries and mirrored headers2026-07-28 has26-07 ✓n/a — mcpx terminates and re-originates+ lowSlow
TR-41Origin validation against DNS rebinding2024-11-05 has 2025-11-25 has mcpx missing24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ 403 · 26-07 ✓ 403 · lootbox —✗ — any Origin accepted on /mcp and /v1 (#204)+ highShigh
TR-42Bind to localhost when running locally2024-11-05 has24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓✓ — loopback unless daemon.address says otherwise+ medSlow
TR-43/mcp mounted on the daemon’s own listeners2025-03-26 hasmcpx design✓ — Unix socket and loopback TCP; serve --transport http gone+ medSlow
TR-44Client drops server requests with string ids2024-11-05 has mcpx missingRequestId = string | number in every revision✗ — ids decoded as int64; frame silently dropped (#203)+ medSmed
TR-45Client HTTP timeout caps whole SSE responsesmcpx missingimplementation✗ — 10 min cap cuts long streams (#203)+ lowSmed

TR-01 stdio framing: one message per line, stdout only MCP

  • What. One JSON-RPC message per line with no embedded newlines; the server writes only valid MCP messages to stdout and the client writes only valid MCP messages to stdin.
  • Where. Every revision. 2026 adds “each message is a single JSON-RPC request, notification, or response” (no batches) and UTF-8.
  • mcpx @ 05c78b2. Server: one object per line through a 64 MiB scanner (internal/mcpserver/server.go:1021-1025), compact JSON per line under a write lock (internal/mcpserver/server.go:1004-1011); mcpx serve switches the CLI to machine output so nothing else reaches stdout (internal/cli/serve.go:378-382); stderr was empty in every recorded stdio run. Client: writes each frame plus a newline under a lock (internal/mcpclient/stdio.go:112-121), skips blank lines and any line that does not start with { or [ (internal/mcpclient/stdio.go:139-142).
  • Value to mcpx. + high: this is the path every spawning host uses.
  • Effort. S — done.
  • Risk. None observed.
  • Detail. Skipping banner lines is accept-liberally against the server’s MUST; the comment calls it deliberate tolerance of servers that log on stdout.
  • Sources. 2024-11-05/basic/transports.mdx:24 “- Messages are delimited by newlines, and MUST NOT contain embedded newlines.”; 2026-07-28/basic/transports/stdio.mdx:13; internal/mcpserver/server.go:1025; internal/mcpclient/stdio.go:139; wire S1–S5.

TR-02 stdio stderr: logging only, not an error signal

  • What. 2024-11-05..2025-06-18: the server MAY write UTF-8 to stderr “for logging purposes”. From 2025-11-25: any logging (informational, debug, error), the client MAY capture, forward or ignore it, and SHOULD NOT treat it as an error indicator. 2026 names stderr as the migration path for the deprecated Logging feature.
  • Where. opencode v1 spawns with stderr: "pipe" and never reads it; a chatty server could block once the pipe fills (inferred from stream back-pressure, not reproduced). opencode v2 drains it line by line into a debug log “so chatty servers cannot stall on a full pipe”.
  • mcpx @ 05c78b2. Server: mcpx serve writes nothing to stderr (wire S1–S5). Client: drains upstream stderr continuously into a 64 KiB ring buffer (internal/mcpclient/stdio.go:92, :101) and quotes its tail only inside error messages (internal/mcpclient/stdio.go:119, :131); it never treats stderr as failure. The pool sets no StderrTo, so upstream stderr is not logged anywhere else (internal/pool/pool.go:369-375).
  • Value to mcpx. + low: done; forwarding upstream stderr to mcpx’s logs would help diagnosis.
  • Effort. S.
  • Risk. Treating stderr as fatal breaks chatty servers; mcpx does not.
  • Detail. Changelog 2025-11-25 minor item 1 (PR #670).
  • Sources. 2024-11-05/basic/transports.mdx:25 “The server MAY write UTF-8 strings to its standard error (stderr) for logging”; 2025-11-25/basic/transports.mdx:31; internal/mcpclient/stdio.go:92; internal/mcpclient/stdio.go:101; v1:packages/opencode/src/mcp/index.ts:348 “stderr: "pipe",”; v2:packages/core/src/mcp/stdio.ts:162.

TR-03 Maximum stdio frame size

  • What. No revision sets a maximum message size, so every implementation picks one, and a sender larger than the receiver’s limit kills the connection.
  • Where. opencode v2 fails the connection if one line exceeds 16 MiB (“MCP stdio frame exceeded 16 MiB”), with an outgoing queue of 64 frames; no cap was found in opencode v1.
  • mcpx @ 05c78b2. Server stdio reads up to 64 MiB per line (internal/mcpserver/server.go:1025); HTTP bodies are read through a 64 MiB LimitReader (internal/mcpserver/server.go:1132), so a larger body is truncated and fails as a parse error. Client stdio and SSE use plumbing.stdioMaxLine, 64 MiB (internal/mcpclient/stdio.go:49; internal/defaults/defaults.json:41; internal/mcpclient/http.go:166).
  • Value to mcpx. + med: an mcpx_exec or mcpx_call result over 16 MiB drops opencode v2’s connection to mcpx.
  • Effort. S — cap or spill results below the smallest known host limit.
  • Risk. One oversized result silently removes mcpx from an opencode v2 Location until reconnect.
  • Detail. The server-side limits are inline numbers while the client’s is a setting, against mcpx’s “nothing hardcoded” rule.
  • Sources. v2:packages/core/src/mcp/stdio.ts:15 “const MAX_FRAME_BYTES = 16 * 1024 * 1024”; v2:packages/core/src/mcp/stdio.ts:117; internal/mcpserver/server.go:1025; internal/mcpserver/server.go:1132; internal/defaults/defaults.json:41.

TR-04 stdio server launch: environment and working directory

  • What. How a client spawns a stdio server: command, arguments, working directory and environment. The spec leaves this to the client.
  • Where. opencode v1 and v2 run command[0] with the rest as arguments, cwd from config resolved against the directory (default: the directory), env = host env plus the config’s environment. v2 spawns through the Location’s environment, so workspace-backed Locations run servers remotely, and with v2’s background daemon “host env” is the daemon’s.
  • mcpx @ 05c78b2. The pool launches with the config’s command, args, env and cwd and InheritEnv: true (internal/pool/pool.go:369-375); the child’s environment is the daemon’s plus the config’s (internal/mcpclient/stdio.go:60-66), in its own process group (internal/mcpclient/stdio.go:69).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. mcpx serve spawned by opencode inherits opencode’s process env, not the per-session shell env (so no MCPX_SESSION_ID); upstreams spawned by the mcpx daemon inherit the daemon’s env, not the caller’s.
  • Sources. internal/pool/pool.go:369-375; internal/mcpclient/stdio.go:60-66; v1:packages/opencode/src/mcp/index.ts:346; v2:packages/core/src/mcp/stdio.ts:84 “extendEnv: true,”.

TR-05 stdio requests handled one at a time

  • What. Over stdio, mcpx handles each request on the read loop, except those that may need to ask the client something. A 120 s mcpx_exec blocks ping, notifications/cancelled and every other request until it returns.
  • Where. No revision requires concurrency, but cancellation and ping assume the receiver keeps reading while it works; 2026 stdio makes notifications/cancelled the client’s MUST.
  • mcpx @ 05c78b2. internal/mcpserver/server.go:1051-1067; mayBlockOnClient covers only tools/call, prompts/get and resources/read, and only for a client that can answer (internal/mcpserver/ask.go:318-327).
  • Value to mcpx. + med: cancellation and liveness on stdio; hosts that ping during long calls stop treating mcpx as hung.
  • Effort. M — dispatch every request on its own goroutine; the writer is already locked.
  • Risk. A host with a ping watchdog kills mcpx during long scripts.
  • Detail. Reply order is the stated reason for in-line handling (internal/mcpserver/server.go:1051-1054), but JSON-RPC does not require ordered replies.
  • Sources. internal/mcpserver/server.go:1063 “} else if resp := s.HandleOn(ctx, c, req); resp != nil {”; 2026-07-28/basic/patterns/cancellation.mdx:42-43; internal/mcpserver/ask.go:318-327.

TR-06 2026 stdio direction rules

  • What. On 2026 stdio the server MUST NOT write JSON-RPC requests to stdout (only responses, request-scoped notifications and listen-stream notifications), and the client MUST NOT write responses. Server-to-client interaction goes through InputRequiredResult.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Modern peers get input_required (internal/mcpserver/ask.go:262); legacy connections still get real requests (internal/mcpserver/conn.go:255-259), which is right per era.
  • Value to mcpx. + med: done.
  • Effort. S — done.
  • Risk. None.
  • Detail. Because stdio shares one channel, clients MUST demultiplex listen notifications by io.modelcontextprotocol/subscriptionId (meta.md SUB-05) and progress by progressToken.
  • Sources. 2026-07-28/basic/transports/stdio.mdx:55 “The server MUST NOT write JSON-RPC requests to stdout.”; 2026-07-28/basic/transports/stdio.mdx:36; internal/mcpserver/ask.go:262.

TR-07 Custom byte-stream transports reuse stdio framing

  • What. 2026 says newline-delimited JSON-RPC works unchanged over Unix sockets or TCP, and custom transports on such streams SHOULD reuse it; only launch, stderr and shutdown need equivalents. Custom transports MUST preserve the per-request metadata model.
  • Where. 2026-07-28. Earlier revisions only say custom transports must preserve the JSON-RPC format and lifecycle.
  • mcpx @ 05c78b2. MCP is served as Streamable HTTP over the daemon’s Unix socket and loopback TCP (internal/daemon/server.go:198-219, :483-488), not as raw NDJSON on the socket.
  • Value to mcpx. + low: an NDJSON-over-Unix-socket endpoint would be a cheap, header-free surface for local agents.
  • Effort. M.
  • Risk. None.
  • Detail. Serving MCP over a Unix socket at all is something mcpx has that the other products compared here do not.
  • Sources. 2026-07-28/basic/transports/index.mdx:75 “Unix domain sockets or TCP) SHOULD reuse the”; 2026-07-28/basic/transports/stdio.mdx:26; internal/daemon/server.go:488.

TR-08 JSON-RPC messages MUST be UTF-8

  • What. From 2025-03-26 the transports page says JSON-RPC messages MUST be UTF-8 encoded; 2024-11-05 mentions UTF-8 only for stderr.
  • Where. 2025-03-26..2026-07-28.
  • mcpx @ 05c78b2. Go’s encoding/json emits UTF-8 (internal/mcpserver/server.go:1004-1011).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. RFC 8259 already requires UTF-8 for JSON exchanged between systems.
  • Sources. 2025-03-26/basic/transports.mdx:5 “JSON-RPC messages MUST be UTF-8 encoded.”; 2026-07-28/basic/transports/index.mdx:29.

TR-09 JSON-RPC batching

  • What. 2025-03-26 adds JSONRPCBatchRequest and JSONRPCBatchResponse: implementations MAY send batches but MUST support receiving them, on stdio and as HTTP POST bodies. 2025-06-18 removes batching from schema and prose.
  • Where. 2025-03-26 only.
  • mcpx @ 05c78b2. mcpx lists 2025-03-26 in Supported (internal/mcpserver/server.go:778) but accepts no arrays: stdio unmarshals into a single request and answers -32700 with no id (internal/mcpserver/server.go:1040-1044; wire S3); HTTP answers 400 with -32700 (internal/mcpserver/server.go:1153-1157; wire W16). The client splits batch arrays on HTTP (internal/mcpclient/http.go:197-210); on stdio it passes them up and recvLoop drops them because they do not decode into a struct (internal/mcpclient/client.go:356-368).
  • Value to mcpx. + low: honest 2025-03-26 support requires it; few clients batch.
  • Effort. M — split arrays, answer with an array of responses, omit notifications; or drop 2025-03-26 from Supported, which is honest but loses clients.
  • Risk. A 2025-03-26 client that batches gets a parse error from a server that claimed 2025-03-26.
  • Detail. 2025-03-26 HTTP: a POST of only notifications or responses gets 202; one containing requests gets JSON or SSE; SSE responses may themselves be batched; initialize must not be batched (lifecycle-versioning.md LV-12).
  • Sources. schema/2025-03-26/schema.ts:17 “export type JSONRPCBatchRequest = (JSONRPCRequest | JSONRPCNotification)[];”; 2025-03-26/basic/index.mdx:97 “batches, but MUST support receiving JSON-RPC batches.”; 2025-06-18/changelog.mdx:12; internal/mcpserver/server.go:1041-1044; wire W16, S3.

TR-10 HTTP+SSE transport (2024-11-05)

  • What. The 2024-11-05 HTTP transport: the client opens an SSE endpoint, the server MUST first send an endpoint event naming the POST URI, the client POSTs every message there, and server messages arrive as SSE message events. Streamable HTTP replaced it in 2025-03-26.
  • Where. Native in 2024-11-05; later revisions describe it only as a compatibility fallback, and the 2026 deprecation registry lists it with an earliest removal of three months after SEP-2596 reaches Final, shorter than the twelve-month minimum stated for everything else. opencode v1 falls back to it for remote servers; v2 does not.
  • mcpx @ 05c78b2. Not served. The client speaks Streamable HTTP to every url: server: transport defaults to http and is not consulted for the wire shape, so transport: "sse" is silently treated as Streamable HTTP (internal/config/config.go:706-708; internal/pool/pool.go:368-381). The SSE reader handles only data: lines and would not recognise an endpoint event (internal/mcpclient/http.go:180-192).
  • Value to mcpx. + med: some hosted servers still speak only HTTP+SSE, and the client fallback is cheap. − serving it is not worth it.
  • Effort. M for the client fallback; L to serve it.
  • Risk. mcpx cannot reach 2024-11-05-only remote servers, and a config asking for SSE is quietly ignored.
  • Detail. The client fallback procedure is TR-11.
  • Sources. 2024-11-05/basic/transports.mdx:61 “The server MUST provide two endpoints:”; 2024-11-05/basic/transports.mdx:67; 2026-07-28/deprecated.mdx:31; 2026-07-28/changelog.mdx:110; internal/pool/pool.go:368 “if p.cfg.Stdio() {”; v1:packages/opencode/src/mcp/index.ts:278.

TR-11 Client fallback from Streamable HTTP to HTTP+SSE

  • What. 2025-03-26: POST an InitializeRequest; if it fails with any 4xx, GET the URL and expect an endpoint event. 2025-11-25: only on 400, 404 or 405. 2026-07-28: POST a modern request; fall back only on 400, 404 or 405 whose body is not a recognised modern JSON-RPC error, otherwise stay modern.
  • Where. Each of 2025-03-26, 2025-11-25 and 2026-07-28 words it differently. opencode v1 tries Streamable HTTP, then SSE, stopping on authentication errors; opencode v2 has no fallback.
  • mcpx @ 05c78b2. No fallback (TR-10; internal/pool/pool.go:368-381).
  • Value to mcpx. + med: a dual-era client should implement the 2026 version, the strictest.
  • Effort. M.
  • Risk. A client following the 2025-03-26 wording classifies a modern server’s 400 -32022 as HTTP+SSE.
  • Detail. 401 is a 4xx, so the literal 2025-03-26 rule makes authentication failures look like an old transport. As a server, mcpx’s 200-status modern errors and non-JSON-RPC 400 bodies (errors.md ERR-07, ERR-13) make a 2026 client’s classification of mcpx less reliable.
  • Sources. 2025-03-26/basic/transports.mdx:271 “- If it fails with an HTTP 4xx status code”; 2025-11-25/basic/transports.mdx:303; 2026-07-28/basic/transports/streamable-http.mdx:729; 2026-07-28/basic/transports/streamable-http.mdx:724-735; v1:packages/opencode/src/mcp/index.ts:269.

TR-12 Streamable HTTP methods: POST+GET → POST only

  • What. Legacy Streamable HTTP MUST expose one endpoint supporting POST and GET, optionally DELETE. 2026 requires only POST.
  • Where. POST and GET in 2025-03-26..2025-11-25; POST in 2026-07-28.
  • mcpx @ 05c78b2. POST handled, GET answered 405, DELETE answered 204 after dropping the session (internal/mcpserver/server.go:1119-1131).
  • Value to mcpx. + low: legacy allows “SSE or 405” on GET, and 405 is the 2026 SHOULD.
  • Effort. S — done.
  • Risk. None.
  • Detail. For DELETE, 2026 says a modern-only server SHOULD answer 405; mcpx’s 204 is right for its legacy sessions (TR-31).
  • Sources. 2025-03-26/basic/transports.mdx:69 “MCP endpoint) that supports both POST and GET methods.”; 2026-07-28/basic/transports/streamable-http.mdx:47; internal/mcpserver/server.go:1119-1131.

TR-13 Standalone GET SSE stream: server side

  • What. 2025-03-26..2025-11-25: a client MAY GET the endpoint to open an SSE stream for unsolicited server requests and notifications; the server returns SSE or 405 and MUST NOT send responses there unless resuming. 2026 removes the GET stream; long-lived notifications use subscriptions/listen over POST, and GET SHOULD get 405.
  • Where. 2025-03-26..2025-11-25; 405 in 2026-07-28.
  • mcpx @ 05c78b2. 405 with Allow: POST, DELETE and a text/plain body “mcpx sends no unsolicited messages; POST a request” (internal/mcpserver/server.go:1124-1131; wire W27).
  • Value to mcpx. + low: correct in both eras.
  • Effort. S — done.
  • Risk. Legacy HTTP hosts never hear a list_changed from mcpx; the capability is truthfully declared false over HTTP (capabilities register).
  • Detail. The 405 body is not JSON-RPC, which is fine for a GET.
  • Sources. 2025-03-26/basic/transports.mdx:134; 2025-11-25/basic/transports.mdx:141 “or else return HTTP 405 Method Not Allowed”; 2026-07-28/basic/transports/streamable-http.mdx:683; 2026-07-28/changelog.mdx:18; internal/mcpserver/server.go:1129; wire W27.

TR-14 Standalone GET SSE stream: client side

  • What. A legacy Streamable HTTP server sends list_changed, resources/updated and server requests outside any POST on the GET stream; a client that never opens it never receives them.
  • Where. 2025-03-26..2025-11-25 (the client MAY open it); replaced in 2026 by subscriptions/listen.
  • mcpx @ 05c78b2. HTTPTransport only POSTs and DELETEs (internal/mcpclient/http.go:97-162, :235-254).
  • Value to mcpx. + med: cache invalidation and resource updates from remote legacy servers.
  • Effort. M.
  • Risk. Remote servers’ tool lists go stale until mcpx refresh.
  • Detail. stdio upstreams deliver the same messages on the pipe. For 2026 upstreams the equivalent is a listen request, which the client also never opens (notifications register).
  • Sources. 2025-11-25/basic/transports.mdx:135-141; internal/mcpclient/http.go:115.

TR-15 Accept: application/json, text/event-stream on POST

  • What. The client MUST send an Accept header listing both application/json and text/event-stream on every POST (and text/event-stream on the legacy GET).
  • Where. 2025-03-26..2026-07-28.
  • mcpx @ 05c78b2. The client sends it on every POST (internal/mcpclient/http.go:80). The server never reads Accept and answers requests that omit it, an accept-liberally choice.
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. None.
  • Sources. 2025-03-26/basic/transports.mdx:88; 2026-07-28/basic/transports/streamable-http.mdx:76 “2. The client MUST include an Accept header listing both”; internal/mcpclient/http.go:80.

TR-16 Request answered with JSON or SSE, chosen per request

  • What. For a POSTed request the server MUST answer with Content-Type: application/json (one object) or text/event-stream (an SSE stream); clients MUST handle both.
  • Where. 2025-03-26..2026-07-28.
  • mcpx @ 05c78b2. Plain JSON unless mcpx has to send the client a request mid-call (legacy elicitation or sampling), in which case the response turns into SSE lazily (internal/mcpserver/server.go:1299-1324; wire L2). Frames carry only data: lines, with Cache-Control: no-cache (internal/mcpserver/server.go:1310, :1319). Without an http.Flusher the stream cannot start, ErrNoPush is returned and the question stays with the broker (internal/mcpserver/server.go:1302-1307). The client handles both content types.
  • Value to mcpx. + med: plain calls pay nothing for streaming.
  • Effort. S — done.
  • Risk. No progress or log notification ever rides the stream (progress and logging registers).
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:88-90; internal/mcpserver/server.go:1311; internal/mcpserver/server.go:1319; wire L2.

TR-17 202 Accepted for notifications (and legacy responses)

  • What. Legacy: a POST carrying only responses or notifications gets 202 with no body, or an HTTP error whose body MAY be an id-less JSON-RPC error. 2026: only notifications get 202, clients MUST NOT POST responses, and 2026 defines no client-to-server notification for HTTP at all (cancellation is closing the stream).
  • Where. 2025-03-26..2025-11-25; narrowed in 2026-07-28.
  • mcpx @ 05c78b2. 202 for a delivered client response (internal/mcpserver/server.go:1142-1150) and for known notifications (internal/mcpserver/server.go:1188-1190; wire W26). An unrecognised notification is answered with a -32601 error and status 200 (notifications register). The client treats 202 and 204 as acknowledged (internal/mcpclient/http.go:132).
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. 2026 leaves header requirements for notification POSTs undefined, so a dual-era server should not reject a header-less notification.
  • Sources. 2025-03-26/basic/transports.mdx:98 “- If the server accepts the input, the server MUST return HTTP status code 202”; 2026-07-28/basic/transports/streamable-http.mdx:95 “This revision of the core protocol defines no client-to-server”; 2026-07-28/basic/transports/streamable-http.mdx:102; internal/mcpserver/server.go:1189; wire W26.

TR-18 Client responses POSTed back to the server

  • What. In legacy Streamable HTTP, a client answers a server request (elicitation, sampling, roots) by POSTing the JSON-RPC response. 2026 forbids clients sending responses at all.
  • Where. 2025-03-26..2025-11-25.
  • mcpx @ 05c78b2. A response POSTed with the session header is routed to the request waiting on that session and acknowledged 202 (internal/mcpserver/server.go:1142-1151; wire L3). With no waiter the client gets 400 and a non-JSON-RPC body (internal/mcpserver/server.go:1146). replyOf parses the id as *int64 (internal/mcpserver/conn.go:306-320), which is enough because mcpx picks its own negative integer ids.
  • Value to mcpx. + med: legacy elicitation over HTTP works end to end.
  • Effort. S — done.
  • Risk. None for mcpx’s own ids.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:81 “The client MUST NOT send JSON-RPC responses.”; internal/mcpserver/server.go:1149; wire L3.

TR-19 Server requests on a request’s SSE stream

  • What. Legacy servers MAY send JSON-RPC requests and notifications on a POST’s SSE stream before the response, and those notifications SHOULD relate to the originating request. 2026: servers MUST NOT send requests on any stream; notifications MUST relate to the originating request; the response SHOULD end the stream.
  • Where. 2025-03-26..2025-11-25; replaced by MRTR in 2026-07-28.
  • mcpx @ 05c78b2. Legacy: elicitation goes out on the POST’s own SSE stream and the answer comes back by separate POST (docs/protocol.md:183; wire L1–L3); the stream carries the result and then ends (internal/mcpserver/server.go:1179-1186). Modern: input_required.
  • Value to mcpx. + med: done per era.
  • Effort. S — done.
  • Risk. None.
  • Detail. The legacy wording is SHOULD for “relate”, 2026’s is MUST.
  • Sources. 2025-03-26/basic/transports.mdx:111 “- The server MAY send JSON-RPC requests and notifications before sending a”; 2026-07-28/basic/transports/streamable-http.mdx:117 “- The server MUST NOT send independent JSON-RPC requests on this stream.”; docs/protocol.md:183; wire L2.

TR-20 Each message on exactly one stream (no broadcast)

  • What. Legacy: a client MAY hold several SSE streams, and the server MUST send each message on exactly one of them. 2026: every request has its own stream; listen notifications go only on the matching listen stream, and request-scoped notifications never go on a listen stream.
  • Where. 2025-03-26..2025-11-25; per-subscription filtering in 2026-07-28.
  • mcpx @ 05c78b2. mcpx opens no GET streams (internal/mcpserver/server.go:1124-1131), so its only streams are per-request ones.
  • Value to mcpx. + low.
  • Effort. S — nothing to do.
  • Risk. None.
  • Detail. 2026’s per-subscription filter replaces “any unsolicited message on any GET stream”.
  • Sources. 2025-03-26/basic/transports.mdx:153 “streams; that is, it MUST NOT broadcast the same message across multiple streams.”; 2026-07-28/basic/transports/streamable-http.mdx:132.

TR-21 SSE resumability via event ids and Last-Event-ID

  • What. 2025-03-26: servers MAY attach SSE ids (unique per session and client); clients SHOULD resume a broken stream with GET and Last-Event-ID; servers MAY replay only that stream’s messages. 2025-11-25: ids SHOULD encode the originating stream, and resumption is always by GET however the stream began. 2026: not supported; a broken stream loses the request and the client MUST re-issue it with a new id; modern-only servers SHOULD ignore Last-Event-ID.
  • Where. 2025-03-26..2025-11-25.
  • mcpx @ 05c78b2. The server writes no id: field (internal/mcpserver/server.go:1319); the client reads only data: lines and never sends Last-Event-ID (internal/mcpclient/http.go:180-192). The Last-Event-ID handling in internal/daemon/server.go:807-823 is mcpx’s own /v1 event feed, not MCP.
  • Value to mcpx. + low: clients re-issue anyway, and replay needs server-side buffering.
  • Effort. M.
  • Risk. None; every part is MAY or SHOULD.
  • Detail. Whether a dropped stream means the client cancelled (legacy: SHOULD NOT be read as cancellation; 2026: MUST be) is in the progress/cancellation register.
  • Sources. 2025-03-26/basic/transports.mdx:162 “1. Servers MAY attach an id field to their SSE events”; 2025-11-25/basic/transports.mdx:186; 2026-07-28/basic/transports/streamable-http.mdx:157 “Resumable SSE streams via Last-Event-ID are not supported.”; 2026-07-28/changelog.mdx:28; internal/mcpserver/server.go:1319.

TR-22 SSE polling: priming event, retry, reconnect

  • What. 2025-11-25: when a server opens an SSE stream it SHOULD immediately send an event with an id and empty data (priming); it MAY then close the connection without ending the stream, SHOULD send retry first, and the client MUST honour retry and reconnect with GET and Last-Event-ID.
  • Where. 2025-11-25 only (SEP-1699).
  • mcpx @ 05c78b2. The client harmlessly skips an empty priming event (internal/mcpclient/http.go:174) but ignores retry: and id: and does not reconnect when the server closes the connection (internal/mcpclient/http.go:180-192).
  • Value to mcpx. + low: only matters for upstreams that opt into polling.
  • Effort. M.
  • Risk. A 2025-11-25 server in polling mode looks as if it dropped mcpx’s request; the result is lost.
  • Detail. None.
  • Sources. 2025-11-25/basic/transports.mdx:107 “ID and an empty data field in order to prime the client to reconnect”; 2025-11-25/basic/transports.mdx:115 “closing the connection. The client MUST respect the retry field,”; internal/mcpclient/http.go:174.

TR-23 X-Accel-Buffering: no on SSE responses

  • What. When starting an SSE response, servers SHOULD send X-Accel-Buffering: no so reverse proxies do not buffer the stream.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Set on every SSE response (internal/mcpserver/server.go:1311; wire L2).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:136 “When initiating an SSE stream, servers SHOULD include the”; internal/mcpserver/server.go:1311.

TR-24 SSE comment lines as keep-alive

  • What. Long-lived streams are encouraged to emit SSE comment lines (starting with :) as keep-alive; clients MUST NOT treat them as malformed. 2026 recommends this in place of ping.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The client skips comment lines (internal/mcpclient/http.go:183-184). The server sends none, including while a legacy call waits on an elicitation answer.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. A proxy with an idle timeout between host and mcpx could cut a long elicitation wait.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:148 “encouraged to periodically emit an SSE comment line (a line beginning with a”; internal/mcpclient/http.go:184.

TR-25 Mcp-Session-Id value and assignment rules

  • What. A legacy server MAY assign a session id in the HTTP response carrying InitializeResult. It SHOULD be globally unique and cryptographically secure, MUST contain only visible ASCII (0x21–0x7E), and the client MUST echo it on every later request; a server that requires sessions SHOULD answer 400 to a request without one. 2025-11-25 spells it MCP-Session-Id (header names are case-insensitive).
  • Where. 2025-03-26..2025-11-25; removed in 2026-07-28.
  • mcpx @ 05c78b2. sess- plus 32 hex characters from crypto/rand (internal/mcpserver/server.go:1278-1284). A request without a session is served statelessly rather than refused (internal/mcpserver/server.go:1214-1218). Idle sessions expire after proto.sessionIdle (30 min), but reaping runs only when another session is issued (internal/mcpserver/server.go:1226, :1265-1276).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. Low.
  • Detail. If crypto/rand fails, newSessionID returns the fixed id sess-0 (internal/mcpserver/server.go:1281), so every session created during the failure would share one id.
  • Sources. 2025-03-26/basic/transports.mdx:187 “initialization time, by including it in an Mcp-Session-Id header on the HTTP”; 2025-03-26/basic/transports.mdx:189; 2025-03-26/basic/transports.mdx:191; 2025-03-26/basic/transports.mdx:197; 2025-11-25/basic/transports.mdx:199; internal/mcpserver/server.go:1283.

TR-26 Mcp-Session-Id minted on server/discover

  • What. 2026 has no sessions: a modern-only server SHOULD ignore an incoming Mcp-Session-Id and neither mint nor echo one.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. sessionFor mints a session for initialize and for server/discover (internal/mcpserver/server.go:1214, :1220-1229), so every modern discover over HTTP gets one (wire W5, W9, M2). docs/protocol.md:181-182 documents this as intended: it is the only way mcpx can bind a requestState for a modern HTTP client (elicitation and MRTR registers).
  • Value to mcpx. + med: conformance. − removing it without another binding breaks MRTR for the clients that do keep the header.
  • Effort. M — bind requestState to something the request carries.
  • Risk. A strict 2026 client ignores the header and so gets no working MRTR anyway; the session map grows by one per discover probe; a gateway that sees the header may start sticky routing.
  • Detail. SEP-2567 names the residual problem for gateways that bridge HTTP to stdio by spawning one subprocess per session: they need another correlation key (authenticated principal, cookie). The process-model register covers how mcpx keys pooled instances.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:685 “- An Mcp-Session-Id header on a request: ignore it, and do not mint or echo”; 2026-07-28/changelog.mdx:12; internal/mcpserver/server.go:1214 “if req.Method != "initialize" && req.Method != "server/discover" {”; docs/protocol.md:181; seps/2567-sessionless-mcp.md:243; wire W5.

TR-27 Mcp-Session-Id minted on a failed initialize

  • What. The legacy session id is assigned “at initialization time” in the response carrying the InitializeResult; a refused handshake has no result to attach it to.
  • Where. 2025-03-26..2025-11-25.
  • mcpx @ 05c78b2. sessionFor runs before dispatch (internal/mcpserver/server.go:1170-1173), so refused handshakes (2024-11-05, 2026-07-28, 9999-01-01) still get a session header (wire W1, W3, W4).
  • Value to mcpx. + low: no phantom sessions.
  • Effort. S — issue after a successful result.
  • Risk. Each failed probe adds a map entry that is reaped only after 30 min and only when another session is issued.
  • Detail. Sending a session for a handshake that never happened is a send-conservatively conflict.
  • Sources. 2025-11-25/basic/transports.mdx:196-200; internal/mcpserver/server.go:1170 “c, issued := s.sessionFor(r, req)”; wire W1.

TR-28 Unknown or expired session id → 404

  • What. A legacy server that has terminated a session MUST answer requests carrying that id with 404, which tells the client to start a new session with initialize.
  • Where. 2025-03-26..2025-11-25. 2026 servers ignore the header.
  • mcpx @ 05c78b2. An id mcpx does not hold (deleted, reaped or invented) falls through to a fresh stateless connection (internal/mcpserver/server.go:1209-1219). Wire L4: a deleted session id got 200 and a 2025-03-26-shaped result.
  • Value to mcpx. + med: clients recover after a daemon restart instead of silently degrading.
  • Effort. S.
  • Risk. After a daemon restart every legacy HTTP host drops to 2025-03-26 shapes and loses elicitation, with no error to notice; opencode’s recovery (TR-29) is triggered only by the 404 mcpx never sends.
  • Detail. This is also why a stateless legacy client is served as 2025-03-26 (TR-35).
  • Sources. 2025-11-25/basic/transports.mdx:211-215; 2025-03-26/basic/transports.mdx:201; internal/mcpserver/server.go:1218 “return s.newConn("", nil), ""”; wire L4.

TR-29 Client re-initializes after a session 404

  • What. A legacy client that gets 404 for a request carrying a session id MUST start a new session with a fresh initialize.
  • Where. 2025-03-26..2025-11-25. opencode v1 (through its SDK patch) re-initializes transparently; opencode v2 treats a 404, or a 400 “Bad Request: Server not initialized”, on a session-carrying connection as an expired session, reconnects and retries once, and applies this only to legacy connections.
  • mcpx (as built, #1637 / PR #1642). The recovery lives in the client layer, not the pool: Client.roundTrip (internal/mcpclient/client.go) catches the narrow trigger — a session-carrying legacy-era request (never initialize itself) answered HTTP 404 — and recovers exactly once. The dead session is forgotten through the transport seam forgetSession() (internal/mcpclient/http.go:669-676; HTTP transports only, stdio has no session to forget), the client re-initializes with no session id attached, and the request is retried exactly once on the new session. The re-initialize is singleflight: reinitMu plus the recovery generation (reinitGen) mean N concurrent 404s from one server restart produce one initialize, and the other calls retry on the session it installs. After a successful re-initialize the tools, resources and prompts list-changed invalidations fire, because a restarted upstream’s catalogue is not the cached one. Everything outside the trigger surfaces as before: a 404 with no session held, a modern-era 404, and a second 404 after re-initialize are ordinary errors. Covered by TestLegacySession404Recovery (internal/mcpclient/reinit_test.go:96); the conformance gap streamable-http-client-reinit-on-404 was retired by PR #1642.
  • Value to mcpx. + med: transparent recovery after a remote server restarts.
  • Effort. M — landed as client-layer recovery in roundTrip (singleflight re-initialize), not a pool reconnect.
  • Risk. Low: the trigger is deliberately narrow (legacy era, HTTP, session-carrying, first 404 only); anything outside it surfaces as an error, as before.
  • Detail. This entry is mcpx as a client of other servers; mcpx’s own server side never sending the 404 is TR-28’s subject.
  • Sources. 2025-11-25/basic/transports.mdx:213-215 “it MUST start a new session”; internal/mcpclient/http.go:139; v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:210; v2:packages/core/src/mcp/client.ts:173; v2:packages/core/src/mcp/index.ts:336; internal/mcpclient/client.go (roundTrip, sessionGone, reinitializeLegacy); internal/mcpclient/reinit_test.go:96; internal/conformance/coverage_transport.go:19.

TR-30 Client sends a session id to modern servers

  • What. 2026 has no sessions, so a modern client has no session id to send.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. The client captures an Mcp-Session-Id from any response (internal/mcpclient/http.go:126-130) and sends it on every later request, modern ones included (internal/mcpclient/http.go:89-93).
  • Value to mcpx. + low.
  • Effort. S — capture only on a legacy initialize.
  • Risk. Low; a modern server should ignore it.
  • Detail. A modern server that wrongly mints one (as mcpx’s own server does, TR-26) gets it echoed back.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:685; internal/mcpclient/http.go:89-93.

TR-31 DELETE handled by the server

  • What. Legacy clients SHOULD DELETE the endpoint with their session id to end a session; the server MAY answer 405. 2026 modern-only servers SHOULD answer DELETE with 405.
  • Where. 2025-03-26..2025-11-25.
  • mcpx @ 05c78b2. Drops the named session and stops its listener; an unknown id still gets 204 (internal/mcpserver/server.go:1119-1123, :1247-1258; wire W28). After a DELETE the same id is served statelessly (TR-28).
  • Value to mcpx. + low: right for a dual-era server.
  • Effort. S — done.
  • Risk. None.
  • Detail. None.
  • Sources. 2025-03-26/basic/transports.mdx:204 “the client application) SHOULD send an HTTP DELETE to the MCP endpoint with the”; 2026-07-28/basic/transports/streamable-http.mdx:683; internal/mcpserver/server.go:1120; wire W28.

TR-32 DELETE sent by the client on close

  • What. The client side of TR-31.
  • Where. 2025-03-26..2025-11-25.
  • mcpx @ 05c78b2. On close, if a session id is held, the client sends DELETE with a 5 s timeout (internal/mcpclient/http.go:236-247).
  • Value to mcpx. + low.
  • Effort. S — done.
  • Risk. None.
  • Detail. The 5 s is an inline constant. Because of TR-30, a modern server that minted an id would also be sent a DELETE.
  • Sources. 2025-03-26/basic/transports.mdx:204; internal/mcpclient/http.go:236-247.

TR-33 MCP-Protocol-Version header requirement timeline

  • What. 2025-03-26: clients SHOULD send it during OAuth server-metadata discovery only. 2025-06-18 and 2025-11-25: clients MUST send it on every request after initialize; if it is absent the server SHOULD assume 2025-03-26; an invalid or unsupported value gets 400. 2026: every POST MUST carry it and it MUST equal _meta protocolVersion, else 400 and -32020; a server supporting pre-2025-06-18 clients MAY treat absence as 2025-03-26, otherwise MUST reject.
  • Where. 2025-03-26 (auth only); 2025-06-18..2026-07-28.
  • mcpx @ 05c78b2. The client sends it on every POST, following the frame’s _meta version or else 2025-11-25 (internal/mcpclient/http.go:78-85; TR-34). The server compares it only with _meta when both are present (internal/mcpserver/server.go:1159-1168; errors.md ERR-07), never uses it as a version (TR-35) and never validates a legacy value (TR-36).
  • Value to mcpx. + med: needed for modern interop and by intermediaries.
  • Effort. S.
  • Risk. See TR-34..TR-36.
  • Detail. A legacy initialize carries no header (only “subsequent requests” must), so a 2025-06-18 server cannot demand it on initialize. 2026 says versions before 2025-06-18 “did not define” the header, which contradicts 2025-03-26’s authorization section (whose example value is 2024-11-05).
  • Sources. 2025-03-26/basic/authorization.mdx:135 “MCP clients SHOULD include the header MCP-Protocol-Version: <protocol-version> during”; 2025-06-18/basic/transports.mdx:242; 2025-06-18/basic/transports.mdx:256; 2026-07-28/basic/transports/streamable-http.mdx:252 “Every POST request to the MCP endpoint MUST include an”; 2026-07-28/basic/transports/streamable-http.mdx:277; internal/mcpclient/http.go:85.

TR-34 Client header value after a legacy handshake

  • What. After a legacy handshake the header SHOULD be the version negotiated in initialize, and a server MUST reject an unsupported value with 400.
  • Where. 2025-06-18, 2025-11-25.
  • mcpx @ 05c78b2. setHeadersFor uses the 2025-11-25 constant unless the frame carries _meta (internal/mcpclient/http.go:81-85); Negotiated is never consulted. The header is also sent on initialize itself, which the spec does not require.
  • Value to mcpx. + med: remote servers pinned to 2025-06-18 become reachable after the handshake.
  • Effort. S.
  • Risk. Every request after initialize fails against a server that negotiated 2025-06-18 or 2025-03-26 and validates the header.
  • Detail. Sending a version the server did not agree to is a send-conservatively conflict.
  • Sources. 2025-11-25/basic/transports.mdx:270-271; internal/mcpclient/http.go:81 “version := ProtocolVersion”.

TR-35 Server uses the legacy header as the version

  • What. From 2025-06-18 the header carries the negotiated version on every legacy HTTP request; the server should assume 2025-03-26 only when it has no other way to identify the version.
  • Where. 2025-06-18, 2025-11-25.
  • mcpx @ 05c78b2. peerFor looks at _meta, then the handshake stored on the connection, then Oldest (internal/mcpserver/conn.go:133-147); the header is only compared with _meta (internal/mcpserver/server.go:1161-1168). A legacy client that stays stateless (never echoes the session id) but sends MCP-Protocol-Version: 2025-11-25 is served as 2025-03-26. Wire L4: a reused session id with header 2025-06-18 got structuredContent flattened to text.
  • Value to mcpx. + med: correct shapes for stateless legacy clients.
  • Effort. S.
  • Risk. Such clients silently lose structuredContent and resource_link.
  • Detail. Guessing downward is recoverable, which is docs/protocol.md’s argument for the floor, but the header is the one signal the spec provides for this case. Once it is used, TR-36 matters.
  • Sources. 2025-11-25/basic/transports.mdx:274-277; internal/mcpserver/conn.go:139-145; wire L4.

TR-36 Invalid legacy header value → 400

  • What. A legacy request with an invalid or unsupported MCP-Protocol-Version MUST get 400.
  • Where. 2025-06-18, 2025-11-25.
  • mcpx @ 05c78b2. The header is checked only when _meta carries a version (internal/mcpserver/server.go:1161). Wire W14: MCP-Protocol-Version: 1999-01-01, no _meta, no session, served with 200.
  • Value to mcpx. + low.
  • Effort. S.
  • Risk. Low.
  • Detail. None.
  • Sources. 2025-11-25/basic/transports.mdx:279-280 “MUST respond with 400 Bad Request”; internal/mcpserver/server.go:1161; wire W14.

TR-37 Mcp-Method header (2026)

  • What. Every 2026 Streamable HTTP request MUST carry Mcp-Method: <method> mirroring the body; a missing or mismatched value gets 400 and -32020 (errors.md ERR-07). The headers are “REQUIRED for compliance”.
  • Where. 2026-07-28 (SEP-2243).
  • mcpx @ 05c78b2. The client never sends it (internal/mcpclient/http.go:78-94). The server never reads it: wire W11 sent Mcp-Method: prompts/list with a tools/list body and got the tool list with 200.
  • Value to mcpx. + high: without it a conformant 2026 HTTP server rejects every modern request from mcpx; as a server, a policy proxy in front of mcpx that routes on the header can be fooled.
  • Effort. S.
  • Risk. High: the modern HTTP upstream path is closed, together with lifecycle-versioning.md LV-26.
  • Detail. Header names are case-insensitive; values are case-sensitive. Validate only modern-era requests, since legacy ones carry no such header. 2026 leaves header requirements for notification POSTs undefined. A -32020 body does not contain -32022, so the mcpx client then falls back to legacy, which a modern-only server also rejects.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:290 “| Mcp-Method | method | All requests |”; 2026-07-28/basic/transports/streamable-http.mdx:293 “These headers are REQUIRED for compliance.”; internal/mcpclient/http.go:78-94; wire W11.

TR-38 Mcp-Name header (2026)

  • What. Mcp-Name mirrors params.name for tools/call and prompts/get and params.uri for resources/read. Values that are not safely ASCII MUST use the sentinel form =?base64?…?=, and plain values that happen to look like the sentinel MUST be encoded too; servers MUST decode before comparing.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. Neither sent by the client nor checked by the server (internal/mcpclient/http.go:78-94).
  • Value to mcpx. + high: required for modern HTTP interop; also lets mcpx, as an intermediary, route or rate-limit by tool without parsing the body.
  • Effort. S, including the sentinel encoding.
  • Risk. As TR-37.
  • Detail. The tasks extension also requires Mcp-Name to carry params.taskId on its task methods (tasks register).
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:291; 2026-07-28/basic/transports/streamable-http.mdx:506 “To avoid ambiguity, clients MUST also Base64-encode any plain-ASCII”; seps/2663-tasks-extension.md:515.

TR-39 Mirror x-mcp-header arguments into Mcp-Param-{Name}

  • What. A server MAY annotate primitive inputSchema properties with x-mcp-header: <Name>; HTTP clients MUST then send the argument’s value as Mcp-Param-{Name}. An absent or null argument means no header; on -32020 the client SHOULD re-list tools and retry; stdio clients MAY ignore the annotation.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. No x-mcp-header or Mcp-Param anywhere in internal/ (internal/mcpclient/http.go:78-94 sets no such header).
  • Value to mcpx. + med: calls to annotated tools on 2026 HTTP upstreams fail -32020 without it.
  • Effort. M — schema walk, value encoding, header emission.
  • Risk. Annotated tools are uncallable through mcpx; intermediaries misroute.
  • Detail. Values use the same sentinel encoding as Mcp-Name; servers SHOULD NOT mark secrets for mirroring.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:364 “While the use of x-mcp-header is optional for servers, clients MUST”; 2026-07-28/basic/transports/streamable-http.mdx:374; 2026-07-28/basic/transports/streamable-http.mdx:408.

TR-40 Intermediaries and mirrored headers

  • What. Intermediaries that do not recognise an Mcp-Param-{Name} header MUST forward it; intermediaries that route or rate-limit on mirrored headers SHOULD reject requests whose MCP-Protocol-Version is absent or older than 2026-07-28, because older versions never required header and body to agree.
  • Where. 2026-07-28.
  • mcpx @ 05c78b2. mcpx terminates and re-originates MCP rather than forwarding HTTP, so this applies only if it ever makes policy decisions on headers (internal/daemon/server.go:488).
  • Value to mcpx. + low: guidance for any future header-based policy.
  • Effort. S.
  • Risk. Trusting unvalidated legacy headers for policy.
  • Detail. None.
  • Sources. 2026-07-28/basic/transports/streamable-http.mdx:548 “Intermediate servers that do not recognize an Mcp-Param-{Name} header”; 2026-07-28/basic/transports/streamable-http.mdx:644.

TR-41 Origin validation against DNS rebinding

  • What. Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding; from 2025-11-25, a present and invalid Origin gets 403 (body MAY be an id-less JSON-RPC error). Requests without Origin (non-browser clients) are unaffected.
  • Where. Every HTTP transport in every revision. lootbox’s /ws endpoint on fixed port 9420 accepts WebSocket upgrades with no Origin check, so any web page can send it a script (cross-site WebSocket hijacking).
  • mcpx @ 05c78b2. No Origin check anywhere on /mcp, which is mounted on the loopback TCP listener as well as the Unix socket (internal/daemon/server.go:483-488); the only middleware records activity (internal/daemon/server.go:290-293, :344-353). Wire W15: Origin: https://evil.example → 200. The same gap covers /v1, where /v1/exec decodes JSON whatever the Content-Type (internal/daemon/routes_exec.go:212-213), so a cross-origin text/plain “simple” request runs a script without a preflight. The TCP port defaults to ephemeral (daemon.port 0, internal/settings/registry.go:343), which raises the attacker’s cost to a port scan.
  • Value to mcpx. + high: mcpx runs tools and scripts as the user, and loopback is exactly what DNS rebinding reaches.
  • Effort. S — reject a present Origin that is not on an allowlist (default none, or loopback), require Content-Type: application/json on POSTs, optionally check Host.
  • Risk. High: a malicious page plus a found port is remote code execution on the workstation.
  • Detail. Unix-socket callers send no Origin and are unaffected. Authentication of /mcp and /v1 is in the auth register.
  • Sources. 2024-11-05/basic/transports.mdx:55 “1. Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks”; 2025-11-25/basic/transports.mdx:79 “- If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden.”; 2026-07-28/basic/transports/streamable-http.mdx:58; internal/daemon/server.go:488; internal/daemon/routes_exec.go:212-213; .lootbox/src/lib/rpc/websocket_server.ts:282-290; wire W15.

TR-42 Bind to localhost when running locally

  • What. When running locally, servers SHOULD bind only to 127.0.0.1 rather than all interfaces.
  • Where. Every HTTP transport in every revision.
  • mcpx @ 05c78b2. The TCP listener binds loopback unless daemon.address says otherwise; the comment calls binding wider “somebody’s decision rather than a default” (internal/daemon/server.go:206-213).
  • Value to mcpx. + med: done.
  • Effort. S — done.
  • Risk. None, but it does not replace TR-41: DNS rebinding reaches loopback.
  • Detail. None.
  • Sources. 2024-11-05/basic/transports.mdx:56 “2. When running locally, servers SHOULD bind only to localhost (127.0.0.1)”; 2025-11-25/basic/transports.mdx:81; 2026-07-28/basic/transports/streamable-http.mdx:63; internal/daemon/server.go:206-213.

TR-43 /mcp mounted on the daemon’s own listeners

  • What. MCP over HTTP is served by the daemon at proto.mcpPath (default /mcp) on both its Unix socket and its loopback TCP port; mcpx serve --transport http now fails and names the address to use.
  • Where. mcpx design.
  • mcpx @ 05c78b2. internal/cli/root.go:190-195 (proto.serveMCP, a lazily built handler); internal/daemon/server.go:483-489; internal/cli/serve.go:373-377; internal/defaults/defaults.json:77.
  • Value to mcpx. + med: one listener and one access decision, so /mcp and /v1 cannot drift apart.
  • Effort. S — done.
  • Risk. The listener is unauthenticated and does not check Origin (TR-41).
  • Detail. Every /mcp call goes back to the same daemon over its own /v1 socket, and the MCP surface is built once on first use; both are in the process-model register.
  • Sources. internal/daemon/server.go:488 “mux.Handle(path, s.MCP)”; internal/cli/root.go:190-195; docs/protocol.md:330.

TR-44 Client drops server requests with string ids

  • What. JSON-RPC ids may be strings or numbers in every revision. A server-initiated request such as elicitation/create or roots/list may therefore arrive with "id": "abc".
  • Where. RequestId = string | number from 2024-11-05 on.
  • mcpx @ 05c78b2. The client decodes incoming frames with ID *int64 (internal/mcpclient/client.go:351-368), so a string id fails to decode and the frame is silently skipped; handleServerRequest takes an int64 (internal/mcpclient/client.go:401). The server then waits out its own timeout.
  • Value to mcpx. + med: correctness against servers whose SDKs use string ids.
  • Effort. S — keep ids as json.RawMessage.
  • Risk. A silent hang, the same class of bug the comment at internal/mcpclient/client.go:346-350 says was fixed once already.
  • Detail. The server side also parses client replies as *int64 (internal/mcpserver/conn.go:306-320), which is fine only because mcpx chooses its own numeric ids.
  • Sources. schema/2024-11-05/schema.ts:53 “export type RequestId = string | number;”; schema/2025-11-25/schema.ts:122; internal/mcpclient/client.go:352; internal/mcpclient/client.go:401.

TR-45 Client HTTP timeout caps whole SSE responses

  • What. The upstream HTTP client’s timeout also bounds reading an SSE response body, so any stream longer than the timeout is cut.
  • Where. Implementation behaviour.
  • mcpx @ 05c78b2. http.Client{Timeout: plumbing.httpRequestTimeout} (10 min) (internal/mcpclient/http.go:54-57, :61). A legacy call that streams for longer than 10 min, such as a slow elicitation, is cut off.
  • Value to mcpx. + low: long elicitations over remote HTTP survive.
  • Effort. S — rely on the context deadline, which pool.callTimeout already sets.
  • Risk. An answer that arrives after 10 min on a remote server is lost.
  • Detail. None.
  • Sources. internal/mcpclient/http.go:61 “hc: &http.Client{Timeout: timeout},”.

Decisions

A decision record exists when two or more issues propose different answers to one question and a choice made later would be a breaking change. Each record says what the code already does, what each issue proposed, which option was taken and why, and exactly what every affected issue has to change as a result. A record that lists options without choosing one does not belong here.

They exist because #181 found eighteen collisions between themes, each between issues written by people who could not see each other’s work, and none with an owner.

#titlestatusdateissues
0001One hook vocabulary, and where a hook’s output goes in the exec streamaccepted2026-09-30#81 #82 #112 #111 · #181 rows 2, 7, 13
0002One autonomy dial, and a ceiling the daemon setsaccepted2026-09-30#111 #112 #113 #114 #110 · #181 rows 3, 5
0003What mcpx declares, and what it enforcesaccepted2026-09-30#82 #177 · #181 row 10
0004Scope-resolved roots: spawn cwd, ${mcpx.root} substitution, initialize workspaceaccepted2026-10-26dezren39/nix#817 #818 #819 · touches #308 #309 #864
0005Release pipeline, version single-sourcing, and the module pathproposed2026-10-29#1881
0006TTLs renew by touch-on-read, bounded by a maxAge ceilingaccepted2026-10-29#1884 · touches #309 #1887 #1893

Status

  • proposed — in a pull request, open for argument.
  • accepted — merged. A record reaches main only by being accepted, so a record on main that says proposed is a bug.
  • superseded by NNNN — a later record changed the decision. The old one stays, with a line at the top naming its replacement; the history of why a thing was decided is worth more than a tidy directory.

A change that contradicts an accepted record changes the record in the same pull request, or it does not merge.

Numbering

Four digits, in the order they were written. A number is never reused, even for a record that was withdrawn before merging.

Template

# NNNN — the decision, as a statement rather than a question

```
number:   NNNN
status:   proposed
date:     YYYY-MM-DD
issues:   #the ones whose text changes because of this · #181 rows, if any
code:     the commit the file:line references were checked against
```

> Two to four sentences: what collided, and what this record settles.

---

## Context

What ships today, with file:line, checked against `code:` above. What each
issue proposes, quoted briefly. Anything found while looking that the issues
did not know — a delivery hole, a stale count, a line that has moved — goes
here, because the decision may depend on it.

## Options

### A — …

What it is and what it costs. Real alternatives only; an option nobody would
take is padding.

### B — …

### C — …

## Decision

The option, stated so it can be implemented without asking. Names, values,
defaults, the API if there is one, and the tests that prove it.

## Consequences

Per affected issue: what changes in it. Then what is now forbidden, and what
is deferred and to which issue.

## Mapping

| term                 | from                           | becomes        |
| -------------------- | ------------------------------ | -------------- |
| every competing term | the issue or file it came from | the chosen one |

0001 — One hook vocabulary, and where a hook’s output goes in the exec stream

number:   0001
status:   accepted
date:     2026-09-30
issues:   #81 #82 #112 #111 · #181 rows 2, 7 and 13 · touches #5 #6 #7 #80 #172 #177
code:     05c78b2

Four proposals for “run something at a moment mcpx notices”, written by people who could not see each other’s work, and one of them already ships. This record keeps the shipped words, says what a hook may be and what it may do, and fixes where its output goes in the one stream mcpx documents as a contract.


Context

What ships

A file script runs inside a generated launcher, a template with named holes (internal/launcher/template.go:17-45). Five of the holes are phases the user fills: before, prefix, onSuccess, onError, suffix (template.go:35-39). They are declared once, as repeatable KindSource settings script.before … script.suffix (internal/settings/registry.go:212-242), so each is a config key, an MCPX_SCRIPT_* variable, a generated flag and a /v1/settings row. They have short spellings on run and exec (--before, --prefix, --on-success, --on-error, --suffix, internal/cli/commands.go:446-455), layer across the config chain with the null-splice rule (internal/config/config.go:208, ResolveLines at :264), and are listed in the man page’s LAUNCHER section (internal/cli/manual.go:427-448).

Their meanings are precise, and the order is tested (TestLauncherPhasesRunInOrder, internal/e2e/e2e_test.go:1437):

phaserunswhere it is written
beforefirst, before mcpx’s globals are installedinternal/runner/runner.go:548
prefixafter the globals, before the script’s module is importedrunner.go:554
onSuccesswhen the entry point returns, with result.value setrunner.go:581
onErrorwhen it throws, with result.error set; the error is rethrown — “a hook, not a handler”runner.go:587, registry.go:233-236
suffixin a finally, on both pathsrunner.go:593

That is a good vocabulary. onSuccess and onError are separate because they see different things; suffix is the one that runs either way. It already answers #112’s own open question — “does onSuccess earn its own phase when post exists?” — with a yes, and says what post would have to mean.

Three holes in its delivery

Found while writing this, each checked against a binary built from 05c78b2. All three are the #177 class: accepted, advertised, and not done.

  1. mcpx exec accepts --before, --on-success and --on-error, and drops them. The inline branch splices only prefix and suffix into the snippet (commands.go:786-799); opts.Phases is set only for a file (commands.go:805-811). mcpx exec --before 'console.error("B")' --on-success 'console.error("S")' 'console.log(1)' prints 1, exits 0, and runs neither hook. The same flags on mcpx run file.ts run both.
  2. A module whose top-level code throws skips onError and suffix. The launcher’s await import(...) sits above its try (runner.go:557 against :569), so an error during import is never caught. suffix is documented as “runs last on both paths, like a finally” (registry.go:240). A throwing exec snippet loses its suffix the same way, since the suffix is spliced after the body.
  3. None of the five reaches /v1/exec or the MCP mcpx_exec tool. execsvc.Options has no field for them (internal/execsvc/execsvc.go:101), Service.Run never sets runner.Options.Phases (execsvc.go:324-343), and mcpx run --remote returns into execOnDaemon before the phases are read (commands.go:556-559, internal/cli/exec.go:472-485). The phases are a local-CLI feature that the settings registry advertises on every surface. RunWith’s comment says this was deliberate — “flattening all of that into the wire Options would make the wire type a mirror of one command’s flag list” (execsvc.go:358-370) — and this record reverses it for the phases, below.

The daemon’s internal hooks, and the bus

pool.Hooks (internal/daemon/hooks.go:24) is a second hook set — OnMessage, OnProgress, OnListChanged, OnResourceUpdated, OnElicitationComplete, Elicit — and every one of them turns into a bus event. It is where #81’s observers get their events, not a vocabulary a user sees.

The bus (internal/events/events.go) declares 16 kinds (events.go:29-52), not the 17 that #81 and #173 count, and two of them — call.started and task.updated — are published by nothing: the only reference to either outside the declaration is events_test.go:67. A hook on them would never fire.

#181 row 13 is right that events.history is 1024 (defaults.json:52, wired at internal/daemon/server.go:165), and it is not the whole answer. Each subscriber has its own 256-event channel (events.go:177, an inline constant the defaults guard does not see), and Publish drops rather than blocks when it is full (events.go:207-234), counting Subscription.Dropped. A hook runner that stalls starts losing events after 256, not 1024. It recovers only by resubscribing from its last delivered seq (SubscribeFrom, events.go:176), and only while that seq is still inside the 1024 retained.

What the issues propose

sourcepointsphaseswhat runswhat it may dodeclared as
#81, approach Ccall, exec, server.start, or any event kindobserve (default), before, aftera command, a JS/TS module, a Python module, a plugin toolobserve: nothing; before: deny or replace; after: rewrite the resulthooks: [{on, phase, run | module | plugin, when, async}]
#82, approach Cas #81as #81the plugin’s own MCP toolas #81registered at runtime over /v1, owned by a plugin session
#112, approach Cexec onlypre, main, post, onError, onSuccessa script or a promptadvise, transform, script, retryoptions.hooks per call, script.hooks in config
the notes, quoted in #112execprompt, prePrompt, postPrompt, onErrorPrompt, onSuccessPrompta prompt—five options

Options

A — adopt #81’s table, and re-express script.* as entries in it

One structured hooks list for everything, including inline source; the five script.* settings and their flags removed.

It renames five shipped settings and five flags, and it pushes the cheapest hook there is — a line of TypeScript that runs inside the script, costs no process and can see result and script — through a structure built for out-of-process actions with timeouts, exit codes and stdin. #81 itself says “do not do that migration in this issue”. And #81’s after merges success and failure into one phase, which is exactly the distinction the shipped set got right.

B — keep all four and document a mapping

Cheapest today. Every later issue picks whichever vocabulary its author read first, and the user learns four. This is the default outcome, and it is why #181 row 2 exists.

C — the shipped phase names become the vocabulary; one structured list for everything that is not inline source

Keep before, prefix, onSuccess, onError, suffix with their meanings, add observe, keep script.<phase> as the spelling of inline hooks, and add one setting for every other kind of hook using the same phase names.

It costs: prefix only means something inside a script; #81’s exec point is renamed; #112’s pre/post/main go; and the registry needs a structured setting kind it does not have.


Decision

C. A hook is four things: where (on), when (phase), what runs (the action), and what its output may do (effect).

Phases

Six, and no others. The five shipped ones keep their names and their meaning, extended from the launcher to any point. One is new.

phasewhenmay hold the operation up
beforebefore the operation startsyes, bounded by a timeout
prefixthe script point only, inline source only: after mcpx’s surface is installed, before the body— (it runs inside the script)
onSuccessthe operation succeeded, before its result is returnedyes, bounded
onErrorthe operation failed, before its error is returned; the error still propagates unless an effect replaces ityes, bounded
suffixafter either, like a finally; it cannot change the outcomeyes, bounded
observeafter the fact, asynchronously, from the event busnever

observe is the default, because it is the only phase that cannot slow or change anything.

Points

With any phase but observe, on names a point: a moment the daemon passes through, enumerated in one Go table the way api.Ops() enumerates routes, each entry with the schema of what a hook receives and may return. The documentation and the validation are generated from the table, and a guard test fires every point and asserts a hook on it ran (#129’s enumerate → completeness → probe), so a point cannot be declared and dead the way call.started is.

pointfires onphases it has
scriptone script run: mcpx run/exec, local or remote, /v1/exec, a recipe or intent runall five
callone tools/call going upstream, from any callerbefore, onSuccess, onError, suffix
server.starta pooled instance startingbefore, onSuccess, onError

#81 called the first point exec. It is script here so that script.onError and {"on": "script", "phase": "onError"} are visibly the same place.

With phase: "observe", on is an events.Filter kind prefix, verbatim — the same vocabulary as /v1/events?kinds=, so "elicit" matches every elicitation event and "*" matches all. when carries the filter’s other fields (server, session, uri), plus tool for the call point.

Actions

Exactly one per hook.

actionwhat runswhere
sourceinline TypeScript or a file, spliced into the launcherinside the script; script point only; declared only as script.<phase>
runan argv array — never a shell string; write ["sh", "-c", "…"] to ask for a shella child process of whichever process owns the point
modulea script run by mcpx’s own runner, with the generated clientas run
prompttext with @placeholders, sent to the provider from #110the daemon; skipped with a warning on the run when no model is configured
plugin"namespace.tool" — an MCP tool callwhichever server or plugin serves that namespace

Python is a run (["uv", "run", "hook.py"]) until #105 makes it a runtime module knows. A shell string is refused rather than guessed at, because sh -c is an injection site and #81 asked for the choice to be loud.

Input: JSON on stdin for run and module, the arguments object for plugin, @placeholders for prompt (JSON-encoded, never text-substituted, as recipes already do), and flat MCPX_HOOK_* variables for the one-line case. Those names go in #172’s outbound table, not beside the code that sets them.

Output: one JSON object whose keys are the levels of the dial in 0002 — advise, ask or refuse, propose, apply, run. A key above the hook’s effect is dropped and reported in its hook frame, never honoured. A non-zero exit is a result, not an error, following the adapter’s precedent (internal/adapter): the hook failed, hook.failed is published, and the operation carries on — unless the hook says "failure": "closed", which is legal only when its effect may refuse (ask or above), because failing closed is a refusal.

Effect

The dial from 0002, per hook, default advise. It is clamped three times: by the phase (suffix at most advise, observe always off), by the caller’s hooks.autonomy, and by the daemon’s autonomy.max.

Declaration: settings, not endpoints

  • script.before … script.suffix stay exactly as shipped. They are {on: script, phase, source}, and the only way to declare a source hook. Nothing about them is renamed.
  • One new daemon-scoped, repeatable setting, hooks: a list of hook objects for every other action, layered across the config chain with the same null-splice rule as script.*. It needs a structured setting kind — a JSON value validated against a schema declared on the setting — which the registry does not have. #110’s model.agents needs the same kind; it is one addition, not two.
  • Per call, ExecOptions gains two fields. script carries the resolved source phases — {before, prefix, onSuccess, onError, suffix}, each a list of lines, the settings’ own shape — so a remote run and a local one run the same launcher. That closes hole 3. hooks appends script-point hooks for that run and may only lower effects. This is #112’s options.hooks, with its phase names replaced.
  • A plugin’s hooks come from its manifest (#80’s mcpx-plugin.json), are resolved when its plugin session starts and dropped when it ends (#82’s approach C). There is no hook-registration route. GET /v1/settings/hooks lists what is in force, and the registry already records where each value came from, which answers #81’s “list them” and #82’s “what is extending this daemon” without a new operation.

The exec frame order (#181 row 7)

One order, replacing the block in docs/exec.md §“The frame order” and the comment on execsvc.Frame (execsvc.go:138-144):

start{runId}
hook{phase:before}                           out-of-script before hooks, in declaration order
log | emit | stdout | call | hook{on:call}   interleaved, as they happen
artifact{id,name,mime,size,sha256,uri}
hook{phase:onSuccess|onError}                in declaration order
result | error                               exactly one, and final
hook{phase:suffix}
end{exitCode,durationMs}
---- only for delivery:"stream" ----
artifact.chunk{id,seq,data}
artifact.end{id}

With four rules:

  1. result or error is sent once, and it is final. Every onSuccess and onError hook finishes before it. #112 put those frames after result while also saying the final result reflects a transform; both cannot be true, and a consumer that reads the answer and disconnects — the reason the order exists — has to be reading the real answer.
  2. What a hook changed is visible. A hook frame carries {on, phase, action, effect, durationMs, changed}, plus original when an apply replaced the value. A caller can always see what mcpx changed.
  3. #111’s call frame is interleaved, beside log, emit and stdout, because calls happen while the script runs. It carries server, tool, a hash of the arguments, duration, ok or error, and destructiveHint.
  4. Frame types are an open set; the order is the contract. A consumer ignores a type it does not know. That is already what the CLI does — applyFrame has no default case (internal/cli/exec.go:199-264) — and the plugin uses the structured shape, not frames. New frame types therefore need no capability flag, which answers #111’s open question: call frames are on by default.

Inline source hooks produce no hook frames. What they log or emit is already part of the run and arrives as log, emit or stdout.


Consequences

#81 adopts this vocabulary. exec becomes script; after becomes onSuccess and onError, or suffix where it must run either way; async is observe; string commands are refused. Slice 1, the observer table, is otherwise unchanged. Its delivery section should use 256 and 1024 as above, and drop call.started and task.updated from the hookable list until something publishes them.

#82 does not add a registration route. Runtime hooks are manifest hooks owned by the plugin session, and teardown is the session ending. A runtime-mutable hook table stays in #82 and waits for a concrete need.

#112 keeps its structure and loses its words: pre → before, post → onSuccess, onSuccess → onSuccess, onError → onError; main is not a hook at all — a prompt that is the run is /v1/intent (#113). Its effect names are replaced by 0002’s, its stream placement by the order above, and script.hooks by hooks entries with "on": "script". The deterministic half it wants to ship first is module and run hooks at the script point.

#111 gets its call frame placed.

#7 is unchanged: placeholders stay the launcher’s. A prompt hook’s @placeholders use the same spelling and the same JSON encoding.

#5 decides one question for both: hooks are declarable wherever script.* is, since a project config can already run code on every mcpx run through script.before. What a project-level config may declare is #5’s call, made once for servers, script.* and hooks.

#6: an observer is a live trigger over the same event vocabulary. Replaying hooks over history needs the store.

#172: MCPX_HOOK_* names are declared in the outbound table.

#177 gets the three delivery holes, independent of everything else here. The fixes are in the report that accompanies this record, not in this change.

Forbidden from here: a hook-registration endpoint; a hook action that is a shell string; a phase outside the six; a source action anywhere but script.<phase>; any frame after result or error that changes it; a point that nothing fires.

Deferred: a runtime-mutable hook table → #82. mcpx hook test <name> --event <file> → #81, as a local CLI command. Hooks registering artifacts → not in the first version; #81 decides. How a run-effect onError retry streams its second attempt → #111 rung 5, bound by rule 1. script.started and script.finished bus kinds, so the script point can be observed → #81.


Mapping

termfrombecomes
script.beforeshippedon: script, phase: before, action source — unchanged
script.prefixshippedon: script, phase: prefix, action source — unchanged
script.onSuccessshippedon: script, phase: onSuccess, action source — unchanged
script.onErrorshippedon: script, phase: onError, action source — unchanged
script.suffixshippedon: script, phase: suffix, action source — unchanged
@before … @suffixlauncher placeholdersunchanged; the launcher’s fill for the phases
pool.Hooksinternal/daemon/hooks.go:24not a user vocabulary; the source of bus events
on: <event kind>#81on: <event kind>, phase: observe
phase: observe#81observe
phase: before#81before
phase: after#81onSuccess and onError, split by outcome; suffix if it must run on both
point exec#81on: script
points call, server.start#81unchanged
run: "cmd …" (a string)#81run: ["cmd", …]; a string is refused
module: x.ts#81module
module: x.py#81run: ["uv", "run", "x.py"] until #105
plugin + tool#81plugin: "namespace.tool"
async: true#81phase: observe
runtime hook registration#82manifest hooks, owned by the plugin session
phase: pre#112before
phase: main#112not a hook: /v1/intent (#113)
phase: post#112onSuccess
phase: onSuccess, phase: onError#112unchanged
script: x.ts#112module (a file), or source through script.<phase>
prompt#112prompt
effect values#1120002’s levels
options.hooks#112ExecOptions.hooks, script point only
script.hooks#112hooks entries with on: script
prePromptnotesprompt action, phase: before
postPrompt, onSuccessPromptnotesprompt action, phase: onSuccess
onErrorPromptnotesprompt action, phase: onError
promptnotes/v1/intent, not a hook
hook frame placed after result#112before result for onSuccess/onError; after it only for suffix
call frame#111interleaved with log/emit/stdout

0002 — One autonomy dial, and a ceiling the daemon sets

number:   0002
status:   accepted
date:     2026-09-30
issues:   #111 #112 #113 #114 #110 · #181 rows 3 and 5 · touches #81 #172 #176
code:     05c78b2

Three issues invented the same dial, and the code already has a fourth spelling of it that any caller can turn up. This record names one dial, says what each stop on it permits, and makes the daemon’s limit a property of the settings registry rather than of whichever feature reaches it first.


Context

The question underneath

Every one of these proposals is answering the same question: when mcpx is about to act on something the caller did not send in the request itself — a script a model wrote, a recipe it picked by score, a repair it worked out, a hook’s output, an operation it routed a sentence to — how far may it go on its own?

That sentence also draws the edge. The script in a /v1/exec body is the caller’s: it sent the code. A tool call is the caller’s. A recipe run by name is not — the caller sent a name, and the code is on the daemon’s search path. A generated script is not. The dial governs the second kind and never the first.

What ships

knobwherevalueswhat it actually does
prompt.modeinternal/settings/consumer.go:97, call scopescript (default), runthe default for /v1/intent; a body mode overrides it unconditionally (internal/daemon/routes_consumer.go:344-347)
mcpx prompt --runinternal/cli/consumer.go:402, :423-426flagsends mode: run
recipe run moderoutes_consumer.go:268-271; mcpx recipes run --scriptscript, run (default)render and diagnose only, or run
prompt.sampleconsumer.go:105, daemon scopenever (default), askwhether mcpx may ask a model at all
elicit.confirmDestructiveconsumer.go:47, daemon scopebool, offask before a destructiveHint call; unanswered refuses
elicit.disambiguateconsumer.go:23, daemon scopenever, askask which live instance to use
diagnose.preflightconsumer.go:65, call scopebool, onrefuse a script whose calls no longer match the schemas
script.typecheckinternal/settings/registry.go:244off, on, strictrefuse a program that does not type-check
elicit.modedocs/elicitation.md §4, status: proposedask, auto, decline, errornot implemented; who answers an upstream server’s question

Three things were found reading this, the third by running it:

  • There is no ceiling. prompt.mode is a default, not a limit. Any caller that can reach the socket sends "mode": "run" and the daemon runs a script a model wrote (routes_consumer.go:393). An operator cannot say “this daemon proposes and never runs”, which is the thing #111’s last paragraph asks for.
  • An unknown mode is silently script. Both routes test mode != "run" / mode == "run" (routes_consumer.go:315, :393), so #113’s plan would be accepted today and quietly mean something nobody chose.
  • A caller’s prompt.mode is accepted and ignored, and so is the runtime API’s. It is declared call-scoped, which promises that “the CLI sends its effective value with the request and the daemon honours it for that request only” (internal/settings/schema.go:87-88). The daemon instead resolves a second, private settings set from its config files and environment once at start (internal/daemon/consumer.go:70-80, settings.Resolve) and reads the policy from that (:128) — never from the request’s callSettings, and never from its own live set. mcpx prompt reads only its own --mode and --run (internal/cli/consumer.go:423-426). Against a matching recipe, MCPX_PROMPT_MODE=run mcpx prompt … and mcpx prompt --prompt-mode run … both answer "mode": "script"; only --run runs it. And PUT /v1/settings/prompt.mode {"value": "run"} answers "applied": true, GET then reports run from runtime:api, and the next prompt still answers "mode": "script" — the API reports a change that did not happen. Half-read, in #177’s terms: read, so TestEverySettingIsReadSomewhere passes, and honoured only from a config file present when the daemon started. The other six call-scoped settings in the same policy (recipes.minScore, recipes.matchMargin, recipes.limit, prompt.catalogBudget, prompt.maxTokens, prompt.runTimeout) go through the same path (consumer.go:117-135) and share the fault.

The settings registry has scopes (internal/settings/schema.go:63-107), and the daemon honours only call-scoped values from a caller’s X-Mcpx-Settings header (internal/daemon/routes_settings.go:50-76). That is the only line between “the daemon’s policy” and “the caller’s preference” in the code today, and it has no way to say “a caller may lower this but not raise it”.

What the issues propose

sourcenamevaluesdefault
#111repair.leveloff, explain, confirm, propose, retry, rewriteexplain
#112hook effectadvise, transform, script, retryadvise
#113prompt.modeadds plan—
#114generators“a generated artefact is a file a human reviews”; never a runtime behaviour—
#110sampling.answerLocallya model answering an upstream question itselfoff
#111, last paragraphClampedBy“the daemon sets the maximum autonomy it will ever exercise, a caller may lower it but never raise it”—

#176 asked for #111’s repair.level to be the one dial.


Options

A — one dial per feature, each with its own words

What happens by default. Four vocabularies for one idea, and a ceiling implemented three times (#181 row 5).

B — #111’s repair.level everywhere, as #176 proposed

It is the earliest and the most worked-out. But the name says repair on a setting that governs prompts and hooks, and level already means log level (logging.level). Its top two stops, retry and rewrite, differ in what runs, not in how far mcpx goes — both execute code the caller did not send, and a ceiling cannot tell them apart. And it has no stop for #112’s transform, replacing the value the caller receives without running anything, so #112 would still need words of its own.

C — one ordered vocabulary named for the concept, instantiated per feature, with one daemon ceiling enforced by the registry

A single set of levels called autonomy; each feature that needs the dial gets a <feature>.autonomy setting; one daemon-scoped autonomy.max bounds them all through a new ClampedBy field on a setting.

Costs: renames the shipped prompt.mode and its body field; adds a registry feature.


Decision

C.

The levels

Ordered from least to most. Each permits everything below it.

levelmaymay not
offnothing — the caller receives exactly what it would have without mcpx’s opinionattach anything
adviseattach text or diagnostics to the result or the errorstop, replace, or run anything
askstop the operation: raise a question through the broker, where an unanswered one refuses, as confirmDestructive already does; or refuse with a structured reasonreplace or run anything
proposereturn a replacement beside the original, unapplied: a script, a plan, a transformed value, a declarationapply it
applyreplace the value the caller receives, with the original kept visibleexecute anything the caller did not send
runexecute code or an operation the caller did not send, with the caller’s own permissions and nothing moreexceed the caller’s permissions, or skip a consent gate

Two rules hold at every level. No level skips a consent gate: elicit.confirmDestructive still asks before a destructive call, whoever wrote the script. No level makes mcpx exceed what the caller could have done — #112’s “a hook must never be able to do what the script it wraps could not”, made general.

The settings

settingscopedefaultreplaces
autonomy.maxdaemon, not hotrunnothing; new
prompt.autonomycall, ClampedBy: autonomy.maxproposeprompt.mode (script → propose, run → run)
repair.autonomycall, ClampedBy: autonomy.maxadvise#111’s repair.level
hooks.autonomycall, ClampedBy: autonomy.maxrun — each hook’s own effect is the real limitnothing; new
hook effectper hook (0001)advise#112’s effect

The mode field on /v1/intent and /v1/recipes/{name}/run becomes autonomy, and an unknown value is a 400 rather than a silent propose. An empty field means prompt.autonomy on the intent route and run on the recipe route, which keeps that route’s own rule — “a route named run runs” (routes_consumer.go:265-271); both are clamped by autonomy.max. mcpx prompt --run and mcpx recipes run --script stay as readable spellings, the way --remote and --local spell exec.where.

autonomy.max defaults to run because that is today’s effective ceiling: no behaviour changes until an operator lowers it. A recipe run by name is governed — the caller sent a name, not the code — so autonomy.max: propose makes the recipe route render instead of run. /v1/exec is not governed, and that is the point rather than a gap: a caller clamped to propose who takes the proposed script and sends it to /v1/exec has run it deliberately, with the code in hand, which is exactly what docs/consumer.md’s trust rule asks for. The ceiling bounds what mcpx does on its own; it does not claim to stop a caller running code the caller sends. 0003 holds it to that claim.

Lowered, never refused

A request above the ceiling is lowered to it and reported, not refused. The answer carries {autonomy, requested, clampedBy}, and the CLI prints one line to stderr. Lowering is always the safe direction, so delivering the lower level is delivering a safe subset of what was asked; refusing would cost the caller a round trip to arrive at the same place. It is not silent — the response says what was asked, what was used, and which setting and which layer decided, which is the provenance the registry already keeps for every value (internal/settings/value.go:47-72).

ClampedBy — an internal/settings capability (#181 row 5)

The API, as it should land:

// internal/settings/schema.go, a new field on Setting:

	// ClampedBy names the setting whose value is the most this one may
	// resolve to. The ceiling is daemon-scoped and not hot, this setting
	// is call-scoped, and both are KindEnum with the same Enum, ordered
	// from least to most permissive: that order is what "most" means.
	//
	// A value above the ceiling is lowered to it, from any layer, and the
	// lowering is recorded on the Value. It is never refused: the lower
	// value is always a safe answer to the request.
	ClampedBy string
// internal/settings/clamp.go, new:

// Clamp is what bounding a requested value by its ceiling produced.
type Clamp struct {
	Value     string // what will be used
	Requested string // what was asked for
	By        *Value // the ceiling and its origin, when it lowered the request
}

// Clamp bounds requested by the ClampedBy ceiling of path, as resolved in s.
// A handler that takes the dial from a request body calls this; the call
// settings header gets it through WithOverrides.
func (s *Set) Clamp(path, requested string) (Clamp, error)

// Rank is the position of raw in an ordered setting.
func (d Setting) Rank(raw string) (int, bool)

Four hooks into existing code, each small:

  1. New (schema.go) refuses a registry where ClampedBy names a missing setting, a ceiling that is not daemon-scoped or is hot, a clamped setting that is not call-scoped, a different Kind or Enum, or a clamped default above the ceiling’s.
  2. Set.raw (value.go:262) returns the clamped value for a setting with a ceiling, so no reader anywhere — consumerPolicyFrom, a handler, a call-scoped view from WithOverrides — can observe a value above it.
  3. Value gains Requested and ClampedBy, and daemon.SettingRecord renders them, so mcpx settings get prompt.autonomy says it was lowered and by what.
  4. The two routes that take the dial in a body call s.callSettings(r).Clamp("prompt.autonomy", body.Autonomy) and put the Clamp in the response. An empty body falls back to the request’s call-scoped value, and the policy is read per request from s.callSettings(r) — the daemon’s live set plus the caller’s header — instead of from the private set initConsumer resolved at start. That closes the half-read above for all seven settings at once, and makes a runtime PUT mean what its "applied": true says.

Version one supports KindEnum only. A floor — a daemon requiring more checking than a caller asks for, as diagnose.preflight might want — is the same mechanism with the order reversed; nothing needs one yet, and elicit.confirmDestructive gets the same effect today by being daemon-scoped so a caller cannot touch it at all.

Landed with autonomy.max, repair.autonomy and hooks.autonomy (internal/settings/clamp.go). Two deviations from the sketch above: a clamped setting’s Enum is an ordered subset of the ceiling’s rather than equal to it, so prompt.autonomy keeps its two levels; and a prompt route whose level is lowered below propose returns neither the script nor a generation, only the candidates and diagnostics. repair.autonomy governs the diagnostics a refused call carries (off drops them); hooks.autonomy governs launcher phases from a configuration file or the environment, never those given as flags on the command itself. The autonomy.max row in internal/api/boundaries.go names the refusal tests.

Originally not implemented, for two reasons. The Setting struct lives in schema.go, which another change is editing now. And a ClampedBy field that clamps nothing would be a declared capability with no behaviour — the #177 bug, and the thing 0003 forbids. It lands with its first consumer: the prompt.mode → prompt.autonomy rename and autonomy.max, which touch internal/daemon/routes_consumer.go, internal/daemon/consumer.go, internal/defaults and docs/configuration.md.

The tests it lands with:

  • New refuses each malformed ClampedBy above.
  • Clamp lowers and never raises; the origin of the ceiling is recorded; a call-scoped override above the ceiling reads as the ceiling; a file layer above the ceiling in the daemon’s own config reads as the ceiling.
  • End to end, with autonomy.max: propose: mcpx prompt --run on a prompt that matches a recipe whose body writes a file returns the script with clampedBy: autonomy.max, and the file does not exist. Falsified by removing the Clamp call: the file appears.
  • End to end, with the default ceiling: MCPX_PROMPT_AUTONOMY=run mcpx prompt … and mcpx prompt --prompt-autonomy run … both run the recipe. Falsified by reading the start-time policy instead of the request’s: both answer propose, which is today’s behaviour for prompt.mode.
  • PUT /v1/settings/autonomy.max answers applied: false, restartRequired: true (routes_settings.go:216-227), because the ceiling is not hot.

What it does not defend against, stated so nobody reads more into it: a caller with socket access can write a higher ceiling into a config file with PUT /v1/settings/autonomy.max {"persist": "user"} and then shut the daemon down. Every daemon setting has that property until the admin operations are access-controlled — the advisory op.admin row in 0003.


Consequences

#111: repair.level becomes repair.autonomy, and each rung has a level (the mapping below). ClampedBy belongs to internal/settings, not to repair; #111 consumes it. “Whose policy wins when the daemon and the caller disagree” is answered: the daemon’s ceiling, with the lowering reported.

#112: effect takes these levels. transform is apply; script is run, or propose to hand the generated script back instead; retry is run. A pre hook that refuses is ask. #112’s “cached, reviewable, promotable” generated script is propose by another name.

#113: prompt.mode: plan is prompt.autonomy: propose. Routing to an operation at run still passes every consent gate, so a destructive operation still asks.

#114: generators are propose by construction — they print a declaration, and a human writes it into config. That is not a setting and cannot be raised; --write <path> is the human’s own act.

#110: prompt.sample and model.provider answer whether a model is consulted at all, which is a different question from how far its output may go. A model answering an upstream server’s question on the user’s behalf is apply, bounded by autonomy.max, whatever #110 names the setting; a question Route sends to a human (internal/elicit/elicit.go:491) is never answered by a model at any level.

Shipped code: prompt.mode is renamed, with no alias — there are no users.

Forbidden from here: a feature-specific autonomy vocabulary; a clamped setting that refuses instead of lowering; any level that skips a consent gate; the dial on anything the caller sent in the request (an exec body, a tool call).

Deferred: floors → the first setting that needs one. Clamping kinds other than KindEnum → the same. Per-feature ceilings (prompt.autonomyMax and so on) → only if one global ceiling proves too coarse; ClampedBy already allows it without an API change.


Mapping

termfromlevel
prompt.mode: scriptshippedpropose
prompt.mode: run, mcpx prompt --runshippedrun
an unknown mode, e.g. planshipped behaviourpropose, silently — becomes a 400
recipe run mode: script, --scriptshippedpropose
recipe run mode: run (the route’s default)shippedrun
off#111off
explain#111advise
confirm#111ask
propose#111propose
retry#111run
rewrite#111run
rung 0, deterministic diagnostic#111advise
rung 1, deterministic near-miss#111propose; run when the fix is provably the intended one and the caller allows run
rung 2, explain#111advise
rung 3, warn / confirm#111ask
rung 4, propose as an artifact#111propose
rungs 5 and 6, retry and reinterpret#111run
refusal with a reason (approach B, item 7)#111ask
pass-through#111off
advise#112advise
transform#112apply
script#112run, or propose
retry#112run
a pre hook that refuses#112ask
observe#81off
a before hook that denies#81ask
a before hook that returns a replacement result#81apply
an after hook that rewrites the result#81apply
prompt.mode: plan#113propose
mode: run#113run
generators#114propose, fixed
sampling.answerLocally#110apply
ClampedBy#111Setting.ClampedBy in internal/settings
a caller lowering the level#111a call-scoped <feature>.autonomy
the daemon’s maximum#111autonomy.max
dryRun#111not the dial: a flag on the caller’s own script
elicit.confirmDestructive, elicit.disambiguateshippednot the dial: consent and clarification gates, unchanged
diagnose.preflight, script.typecheckshippednot the dial: checks on the caller’s own code
prompt.sample, model.providershipped, #110not the dial: whether a model is consulted
elicit.modedocs/elicitation.md, proposednot the dial: who answers an upstream question, by rules the user wrote

0003 — What mcpx declares, and what it enforces

number:   0003
status:   accepted
date:     2026-09-30
issues:   #82 #177 · #181 row 10 · touches #80 #81 #111 #129 #172 and every issue whose footer says "Security/auth deferred; see tracker"
code:     05c78b2

Every issue in the backlog defers security to the tracker, and #82 proposes declaring capabilities now and enforcing them later. Nobody owned the line between the two. Looking for it turned up a daemon whose documented access control is not its access control, a sandbox that holds on one runtime of three, and a credentials block that does nothing at all.


Context

What the issues say

  • #82, approach C: “Capabilities become declarable. The plugin manifest says what it intends to use … and the daemon can report and later enforce that. Declaring is useful even before enforcing, because it is documentation the daemon can print.” And: “capability enforcement is security work and is out of scope here”.
  • #181 row 10: “Nobody owns the declared-vs-enforced capability boundary … The deferral has nowhere to land.”
  • #177: a declaration that does not do what it says is a bug, and “it is indistinguishable from success”.
  • docs/protocol.md:18-32: “Accept liberally, send conservatively … a notification or a server-initiated request that the peer did not negotiate and did not declare is a lie about what was agreed”. Capabilities that depend on a push are “declared only when a notifier is behind them” (:108-110).
  • internal/api/ops.go:59-67: Admin is “carried here so a later OAuth pass has one place to read scopes from, and so the MCP tool can say so in its description”. Mutating and Destructive feed the MCP annotations.

What exists, and what each declaration actually does

Everything below was checked against a binary built from 05c78b2, in an isolated state directory, and the daemon stopped afterwards.

1. The daemon’s access control is not what it is documented to be. The OpenAPI document says: “Every endpoint is unauthenticated. The socket’s file permissions are the access control” (internal/api/openapi.go:38-39); daemon.endpoint says the local socket has “filesystem permissions as the access control” (internal/settings/registry.go:354-355). The socket is indeed 0600 (internal/daemon/server.go:202). But every daemon also listens on loopback TCP, unconditionally, on an ephemeral port (server.go:209-213, daemon.port default 0), and that listener serves the same unauthenticated API with no check of any kind — no Origin, no content type, no token. A request shaped exactly like a browser’s no-preflight “simple request” runs code:

curl -X POST http://127.0.0.1:$PORT/v1/exec \
  -H 'Content-Type: text/plain;charset=UTF-8' -H 'Origin: https://evil.example' \
  -d '{"source":"console.log(\"ran from a foreign origin\")","options":{"output":"structured"}}'
→ "stdout":"ran from a foreign origin\n"

and /mcp answers initialize for the same foreign Origin. The MCP Streamable HTTP transport (2025-11-25, §“Security Warning”) says servers “MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks. If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden.” The OpenAPI text does mention the TCP listener, but only to say that binding it wider than loopback is a deliberate act — it treats loopback as private, and loopback is not private: every other user on a shared host can reach it, and so can a web page, subject only to whatever the browser does about pages reaching the local network, which varies by browser and version. The port is ephemeral, not secret; the range is 16,384 ports on macOS and about 28,000 on Linux, and a caller that fires the same request at all of them does not need to know which one answered. Scripts run with --allow-all by default (internal/runner/runner.go:52-53). The declared boundary is the socket’s mode bits; the enforced boundary is “can send an HTTP request to localhost”.

2. script.permissions holds on Deno and nowhere else. The setting offers all, net, read, read-net and strict (registry.go:183-189), and its help does not mention runtimes. The profiles become Deno flags (runner.go:41-67), which are appended for Deno and never for Bun or Node (runner.go:85-101). The code’s own comment says “bun and node run with the user’s own authority whatever is asked, which is stated here rather than pretended otherwise” (runner.go:43-45) — stated in a Go comment, to nobody who sets the value. With a script that reads a file outside its working directory: --runtime deno --permissions strict fails with NotCapable; --runtime node and --runtime bun print the file and exit 0. Under script.runtime: auto, whether strict holds depends on whether Deno happens to be installed, which on a Nix install it always is (#181 row 11), so nobody testing through the flake sees it.

Supersession note (2026-10-31, #3246): the paragraph above describes the state when this record was written. It no longer holds as written: the profiles now live in internal/defaults/defaults.json, enforcement is rendered per runtime in internal/runner/runtimes.go, node enforces readnet / read-net via --permission --allow-fs-read=*, and a named profile with no entry for the chosen runtime is refused with an error rather than run unenforced. The corrected per-runtime description is the “is it safe?” answer in docs/faq.md. The original text above is kept unchanged as the record of the problem this decision addressed.

3. servers.<name>.auth does nothing. config.Server.Auth (internal/config/config.go:141) is the only non-test reference to package mcpauth in the tree; nothing calls Auth.Resolve (mcpauth.go:93) or Auth.Describe (mcpauth.go:190). A bearer token in auth is parsed and never sent. docs/story.md:2013-2027 documents it as working, including “an unset variable is named up front” and “a server requiring [OAuth] says so before the first request”; Resolved.NeedsOAuth (mcpauth.go:84-86) is read by nothing. #115 lists it as an existing capability. docs/protocol.md:378 (“OAuth for remote servers. Declared and not performed”) is honest about OAuth, and says nothing about the rest of the block.

4. One MCP annotation is wrong, and one label reads like a permission. The annotations are hints by the specification’s own definition — “all properties in ToolAnnotations are hints … Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers” (2026-07-28 schema, ToolAnnotations). But idempotentHint is computed as !Mutating || Destructive (internal/api/ops.go:664), which calls restart idempotent; a second restart kills in-flight calls a second time. And an admin operation’s tool description begins “Privileged:” (ops.go:675-677), an mcpx-invented label, while nothing checks who calls it. The OpenAPI document states that plainly; the tool description, which is what a model reads, did not. The design behind it is sound and written down — “withholding the tool buys no safety” (docs/parity.md:65-69, internal/mcpserver/ops.go:25-29) — only the label was ambiguous.

5. Routing is not permission. A question whose single boolean field is named confirm is routed to a human, because “consent is the one thing an agent cannot give on somebody’s behalf” (internal/elicit/elicit.go:508-512). Broker.Respond (elicit.go:292) accepts an answer from anyone, and By is self-reported. That is correct for what it is — mcpx cannot know who is answering — and it is a hint, not a guarantee.

6. What is enforced, and says so. The daemon honours only call-scoped settings from a caller’s X-Mcpx-Settings header (routes_settings.go:64-72). A non-hot setting written at runtime is not applied and the answer says restartRequired (routes_settings.go:216-227). The socket and its directory are 0600/0700 (server.go:202, paths.go:144-160). Capabilities to peers follow the promise rule in docs/protocol.md §2.2 and §4. ExecOptions’ capabilities: ["artifacts"] is the caller’s declaration and is trusted by design, because remoteness cannot be read off a transport (internal/execsvc/execsvc.go:65-72). None of the restrictions in this paragraph — the header filter, the socket’s mode, the directory’s mode — has a test that tries to get round it.

The pattern: promises get delivered and guarded, because the protocol and settings work built guards for them. Restrictions are enforced where enforcement was cheap and silently absent where it was not. Hints are a mix of true, false, and phrased like rules.


Options

A — declare nothing until it is enforced

Strip the “Privileged” label, the audience reasons and #82’s manifest capabilities until something enforces them. It throws away real value: the MCP annotations are hints by specification, a model is better for reading them, and #82 is right that a declaration the daemon can print is documentation. It also would not have caught findings 1–3, which are not early declarations but broken ones.

B — declare freely; each feature enforces what it can

The status quo, and it produced every finding above.

C — three kinds of declaration with one rule each, one owner, one table, two guard tests

Distinguish promises, restrictions and hints; give restrictions exactly two states; put every restriction in one table owned by the surfaces that render it; guard both states.


Decision

C.

Three kinds of declaration

kindsaysrule
promisemcpx will do X: a capability to a peer, a setting’s effect, a config field’s effect, a documented behaviourdelivered. There is no advisory promise; an undelivered one is a bug (#177), not a state.
restrictionX will be refused: a sandbox profile, a scope filter, a ceiling, a plugin’s capability list, an auth scope, an access controlenforced — mcpx, or something it drives, refuses X, and a test tries X and sees the refusal — or advisory — nothing refuses X, and every surface that shows the restriction says advisory in the same text. No third state.
hintX is probably true, act accordingly: MCP annotations, the admin label, an elicitation’s audiencetrue, and recognisable as a hint: a field the specification defines as one (MCP’s *Hint), or the word advisory beside it. A false hint is a bug.

“Declarable now, enforceable later” is advisory, and it may be declared today — on the condition that the word is on every surface that shows it. It moves to enforced only in the change that adds its refusal test. Nothing may be shown as enforced before that test exists.

Ownership

The daemon is the only enforcer. A plugin, a hook, a manifest and a caller only declare.

SURFACES owns the declarations, in one table in internal/api, beside the ops table: api.Boundaries(). The ops table already names itself “the one place a later OAuth pass reads scopes from” (ops.go:59-61), and a restriction on a setting, an operation or a plugin reaches a caller only through surfaces SURFACES owns — tool descriptions, OpenAPI, /v1/settings, the CLI. Every restriction any issue wants to declare is a row: #82’s manifest capabilities, 0002’s autonomy.max, 0001’s fail-closed hooks, a future OAuth scope. The footer “Security/auth deferred; see tracker” on every issue now has somewhere to land: a deferred restriction is an advisory row naming the issue that will enforce it.

// internal/api/boundaries.go

type BoundaryState string

const (
	Enforced BoundaryState = "enforced"
	Advisory BoundaryState = "advisory"
)

// Boundary is one restriction mcpx declares.
type Boundary struct {
	Name     string // "daemon.access", "script.permissions", "op.admin"
	Declares string // the restriction, in one sentence
	State    BoundaryState
	// Refusal names the case in TestEveryEnforcedBoundaryRefuses that tries
	// to get round it. Required when enforced; a claim with no probe is the
	// thing this table exists to stop.
	Refusal string
	// Until names the issue that will enforce it. Required when advisory.
	Until string
}

func Boundaries() []Boundary

The guard tests

Following #129’s enumerate → completeness → probe, and #177’s second warning that acceptance is not effect:

  1. TestEveryEnforcedBoundaryRefuses, end to end, against the real binary: one case per enforced row, each doing the forbidden thing and asserting the refusal and that the same action without the restriction succeeds (#75’s with-and-without). Its first cases: a daemon-scoped setting in X-Mcpx-Settings leaves the daemon’s value unchanged; a read outside the working directory under script.permissions: strict, for every runtime found on the machine; a TCP request without the daemon’s credential, and a request with a foreign Origin, are refused.
  2. TestEveryBoundaryIsEnforcedOrSaysAdvisory, a unit test over api.Boundaries(): an enforced row must name a case that exists in test 1 (the completeness half, so the table cannot claim what the probe does not try); an advisory row must name an issue, and every surface that renders it must contain advisory.

The two together mean a boundary cannot be declared enforced without a probe, and cannot be declared at all without saying which it is.

The rows it starts with

With the state each has when the table and its tests land together — which they must, since an enforced row without its case fails test 2.

rowdeclaresstaterefusal case, or until
daemon.accessonly a process that can open the socket or read the daemon record — or was given its token — can use the APIenforced after the fix belowTCP without the token; foreign Origin
settings.callScopea caller’s header changes only call-scoped settingsenforceddaemon-scoped key in the header
script.permissionsa script cannot exceed its profileenforced after the fix belowoutside read, per runtime
op.adminadmin operations would need a separate grantadvisorythe OAuth pass — a new issue under #181
elicit.audiencea question routed to a human is answered by oneadvisory#82, once answerers have an identity
plugin.capabilitiesa plugin uses only what its manifest listsadvisory#82, one capability at a time
autonomy.maxmcpx does not act above the ceiling on its ownenforced, from the day it lands0002’s end-to-end case
hooks.failCloseda failing fail-closed hook refuses its operationenforced, from the day it lands0001’s hook dispatcher

What changes now

  • daemon.access is the most urgent thing in this record, and it is a security fix rather than a documentation one. Two steps. First, the TCP listener refuses, with 403, any request whose Origin header is present and not allowed: cheap, the specification’s MUST, and it closes the browser path, since browsers always send Origin on a cross-origin POST and the CLI and the plugin never do. Second, it requires a credential that only a reader of the 0600 daemon record can have: a random token generated at start, written into daemon-<key>.json beside the endpoint, and sent as Authorization: Bearer by every client that discovers the endpoint there. A client of a daemon on another machine gets the token the way it gets the endpoint, from its own configuration — a setting beside daemon.endpoint. The unix socket stays as it is. Then “file permissions are the access control” is true of both listeners. The fix is in internal/daemon/server.go and the clients that read the record (internal/cli/client.go, plugin/opencode/mcpx/daemon.ts, docs/plugin-discovery.md).
  • script.permissions: a profile other than all on a runtime with no permission model is refused before anything runs, naming the runtime and the profile. Refusal rather than an advisory label, because a sandbox that silently does not apply is the worst version of this bug. The fix is in runner.Detect/runner.Permissions, and #105 inherits the rule for Python.
  • servers.<name>.auth: wired, or the field removed. A config block that parses and does nothing is a false promise; until it is fixed, mcpx config and mcpx doctor should say it is ignored. The docs/story.md section waits for the final docs pass, which owns that file.
  • idempotentHint comes from an explicit per-operation field rather than a formula over two unrelated ones; restart is not idempotent.
  • The admin label says advisory in the tool description. Done in this change: internal/api/ops.go and TestAPrivilegedOperationSaysItIsAdvisory.

Consequences

#82: manifest capabilities ship as advisory rows, and every listing of them says “advisory”. Each becomes enforced separately, with its own refusal case, once the plugin session identifies callers. The plugin session must carry an identity the daemon can check, or no capability can ever leave advisory.

#181 row 10 closes: the boundary has an owner, a table and two tests.

#177 gains four instances: the TCP listener, script.permissions on Bun and Node, the inert auth block, and restart’s idempotentHint. The first needs to be treated as a security issue; there are no users yet, which is the only reason it is not an emergency.

#111, #112, #81: ClampedBy and fail-closed hooks are restrictions, so they land enforced, with their refusal cases, or not at all.

#129: the two tests are guards of its kind; api.Boundaries() is the registry they walk.

#80, #83: a plugin or tool source that declares anything restrictive adds a row.

Peer capabilities — what mcpx declares in initialize or per request — are promises, and stay under docs/protocol.md’s rule and the PROTOCOL area’s guards. “Send conservatively” is the promise rule applied to the wire: do not send what the peer did not declare, and do not declare what mcpx will not deliver.

Forbidden from here: a restriction shown without its state; an enforced row without a refusal case; an advisory row without the issue that will enforce it; any surface describing an access control that a test has not tried to get round.

Deferred: authenticating callers — who is calling, not just whether they can reach the socket — to the OAuth pass the ops table already anticipates. Until then every row about who may do something is advisory, and says so.


Mapping

termfromkindstate
“the socket’s file permissions are the access control”openapi.go:38, registry.go:354restrictionnot true of the TCP listener → daemon.access, enforced after the fix
loopback TCP listenerserver.go:209-213—the unguarded half of daemon.access
Origin validationMCP Streamable HTTP, 2025-11-25restrictionmissing; a MUST
capabilities declared to a clientdocs/protocol.md §2.2promisedelivered
sampling declared to serversdocs/protocol.md:271, #39promisedelivered only with a handler
“send conservatively”docs/protocol.md:18-32promise, on the wirePROTOCOL’s guards
readOnlyHint, destructiveHint, openWorldHintops.go:659-667hintspecification-defined; true
idempotentHintops.go:664hintfalse for restart → explicit field
Admin, x-mcpx-adminops.go:59-62, openapi.go:58hintthe OpenAPI text says so
“Privileged:” in a tool descriptionops.go:676hintnow says advisory
Mutating, Destructiveops.go:63-67hintthrough the annotations
call-scope filter on X-Mcpx-Settingsroutes_settings.go:64-72restrictionenforced; gains its refusal case
Hotschema.go:183-188, routes_settings.go:216-227promisedelivered: not applied, and the answer says so. The field’s comment says the API “refuses it” (schema.go:187); it answers 200 with applied: false
socket modeserver.go:202, paths.go:156-160restrictionenforced by the OS
script.permissionsregistry.go:183, runner.go:41-101restrictionDeno only → refuse elsewhere
elicitation audienceelicit.go:74-84, :491-514hintadvisory until #82
servers.<name>.authconfig.go:141, docs/story.md:2013promiseundelivered → wire or remove
NeedsOAuth, “says so before the first request”mcpauth.go:59-64, :84-86promiseundelivered
OAuth “declared and not performed”docs/protocol.md:378a documented gaphonest; a gap written down is not a promise
ExecOptions.capabilitiesexecsvc.go:65-72the caller’s promisetrusted by design
manifest capabilities#82, approach Crestrictionadvisory until #82 enforces each
“declarable now, enforceable later”#82—advisory
ClampedBy, autonomy.max0002restrictionenforced from the day it lands
failure: closed0001restrictionenforced from the day it lands
“Security/auth deferred; see tracker”every issue’s footer—an advisory row naming its issue

0004 — Scope-resolved roots: spawn cwd, ${mcpx.root} substitution, initialize workspace

number:   0004
status:   accepted
date:     2026-10-26
issues:   dezren39/nix#817 #818 #819 (design source, consolidated here) ·
          implementation tracked in this repo, see "Slices" below ·
          touches #308 #309 #864
code:     pkgs/mcpx at the merge commit of this record's PR

mcpx scopes already decide which process serves a caller, resolving the caller’s cwd to a git root to do it. But nothing tells that process where the caller is: the child spawns in the daemon’s cwd, argv and env are the config’s static strings, and the initialize handshake carries no workspace. Three issues in dezren39/nix (#817, #818, #819) proposed the three halves of one fix from a single design note (2026-10-25). This record consolidates them into one design, decides the questions they left open, and is the design’s home from now on: the source lives in this repo, so the design lives here too. The nix issues stay open until the #309 re-homing pass closes them with pointers; their acceptance criteria carry over verbatim into the implementation issues filed from this record.


Context

What the code does today (verified at the branch point of this record)

  • internal/config/scope.go — Scope.Key(ctx CallContext) resolves the caller’s cwd via locateGit (internal/config/gitlocate.go, native discovery with a git rev-parse fallback; gitWhere carries commonDir, worktree, noWorktree) and returns an opaque key only (repo:<common-dir>, worktree:<top-level>, cwd:<dir>, global, session:…, psession:…, pid:…, call:…). The resolved root is computed and then thrown away.
  • internal/daemon/registry.go — keyFor(p, cc) computes the key from the CallContext; internal/pool/pool.go — Acquire(ctx, key) receives only the key. startLane → dial() builds StdioOptions{Cwd: p.cfg.Cwd} verbatim (internal/mcpclient/stdio.go sets cmd.Dir = opts.Cwd). A repo- or worktree-scoped server therefore still runs in the daemon’s cwd.
  • internal/config/config.go — the Server struct holds Command/Args/Env/Cwd with no placeholders; there is no templating anywhere in internal/config or internal/pool.
  • internal/mcpclient sends no rootUri and no workspaceFolders at initialize (grep: none).
  • internal/pool/eras.go — Identity(c *config.Resolved) hashes the static spawn definition (command, args, env, cwd, transport, URL, headers) for the era cache.
  • Instance lifecycle is keyed sharing: instances are keyed by (server, scope key), capped (pool.max, default 4), LRU-evicted at capacity, stopped after IdleTimeout (default 5 min) by ReapIdle, and stopped eagerly for caller-owned keys (CallContext.CallerOwned). None of that changes here.

What each issue proposed

  • nix#817 (step 1) — when a server’s scope is repo/worktree and its config cwd is empty, spawn the instance with Dir = the resolved scope root. The scope resolution must hand the pool the root, not just the key. Explicit config cwd always wins.
  • nix#818 (step 2) — two placeholders, substituted at instance-spawn time only: ${mcpx.root} (the scope-resolved root) and ${mcpx.cwd} (the caller’s cwd), in Args, Env and Cwd. Stored config stays static; substituted values must feed the era-hash inputs; unknown or malformed placeholders are a loud error, never passed through.
  • nix#819 (step 3) — when an instance has a scope-resolved root, send it at initialize as rootUri + workspaceFolders, so workspace servers (gopls) start a real workspace session. global instances send nothing — no fabricated daemon-cwd root.

The casualty that motivates all three

gopls is a workspace server whose workspace is its process cwd and its initialize root. Behind mcpx today it loads files ad hoc (“command-line-arguments” package) and go_package_api on a module path returned empty after 156 s (live fleet, 2026-10-25). Flipping gopls to "scope": "worktree" before these land only multiplies processes; the config flip is deliberately the last slice.


Decision

1. One resolution object: ResolvedScope

Scope.Key gains a sibling in internal/config:

type ResolvedScope struct {
    Key      string // exactly what Key() returns today
    Root     string // the scope-resolved directory, "" when the scope has none
    Degraded string // as Key() reports today
}

func (s Scope) Resolve(ctx CallContext) ResolvedScope

Key() becomes Resolve(ctx).Key so there is one resolution path, not two. Root semantics per scope:

scopeRoot
worktreethe caller’s checkout top-level (gitWhere.worktree)
repothe main worktree: the parent directory of the common dir, only when the common dir’s base name is .git; otherwise "" (bare repos and submodule-style common dirs whose parent is not a worktree root get no root — never guess)
cwdthe caller’s cwd, canonicalized exactly as the cwd: key canonicalizes it
worktree/repo degraded to a cwd: key (outside a repo, unreadable repo)that same cwd — the instance is effectively per-cwd, so rooting it there is consistent, and the degradation is still logged as today
global, session, psession, pid, call, and any fallback key"" — these scopes have no meaningful root, and an empty root is what keeps global instances from fabricating one

Root is a pure function of the key for repo/worktree/cwd scopes, which is what makes the plumbing in §2 safe: two callers who share a key always resolve the same root.

2. Plumbing: the pool learns the root at acquire time

Registry.keyFor already holds the CallContext; it switches to Resolve and passes the ResolvedScope alongside the key into Pool.Acquire. The pool stores root on the Instance at creation; startLane/dial() read it from the instance being started (including restarts and era-lane redials — the root cannot change for a given key, so re-resolution at redial is unnecessary and must not happen).

3. Spawn cwd (from #817)

Stdio only. The effective spawn cwd is, in order:

  1. the config’s cwd, after §4 substitution, when non-empty — explicit config always wins;
  2. the instance’s resolved root, when non-empty;
  3. unset (today’s behavior: the child inherits the daemon’s cwd).

HTTP/SSE servers have no process cwd and are unaffected.

4. Substitution (from #818)

At spawn time only, on the effective spawn definition, in Args, every Env value, and Cwd:

  • ${mcpx.root} → the instance’s resolved root;
  • ${mcpx.cwd} → the caller’s cwd from the CallContext that created the instance (canonicalized as in §1).

Rules:

  • Substitution never touches the stored or merged config; mcpx config output keeps showing the placeholders.
  • Placeholder names are validated at config load: any token matching ${mcpx.<name>} with an unknown <name>, and any malformed ${mcpx. sequence, is a config error naming the server and the field. Only the mcpx. namespace is mcpx’s — every other ${…} form passes through verbatim, exactly as today.
  • If a server’s spawn definition references ${mcpx.root} and the instance’s root is "" (e.g. the server is global-scoped), the spawn fails loudly naming the server and field. Silently substituting an empty or daemon-cwd root is the failure mode this design exists to remove.
  • The substituted definition is what pool.Identity hashes for that instance’s era record: two instances of one server differing only by substituted root must not share an era entry. (Today Identity reads the static Resolved config; the slices must move the hash input to the effective per-instance definition and verify this holds, per #818.)

5. Initialize workspace (from #819)

mcpclient.Options gains the instance root. When it is non-empty, the initialize mcpx sends for that instance includes:

  • rootUri: the root as a file:// URI;
  • workspaceFolders: [{uri: <same>, name: <base name of the root>}];
  • the workspace.workspaceFolders client capability declared true.

Both fields are sent for every era mcpx speaks (legacy and modern): initialize params are extensible, servers that do not consume workspace fields ignore them, and era-specific withholding would need per-server quirks this design refuses to grow. When the root is empty, initialize is byte-for-byte what it is today. Servers that connect over HTTP get the same treatment — the root describes the caller’s workspace, not the transport.

6. Observability

Every instance spawn logs one line — server, key, root, and the cwd source (config / scope / daemon) — at the same level the daemon already logs scope degradations. mcpx status instance JSON gains a root field. The spawn-cwd decision must be testable without a live server (a pure function of scope + CallContext + config cwd).

7. codedb and codebase-memory: deliberately global

Decided, closing the open question from the 2026-10-25 design note: one shared codedb, global scope, is the fleet design and stays. codedb and codebase-memory are multi-project inside one process — their indexes are keyed by project path and the project is a per-call argument — so per-project instances would duplicate indexes and buy nothing. This decision is about the default, not a limitation: §4 is what lets an argv-rooted server (codedb’s index root is baked into its argv in the fleet config today) express a per-scope root if a future config ever wants one.

The shared codedb’s index root stays a per-machine config concern (the machine’s git tree: /home/user/git on the laptop, ~/git on the sandbox), living in the machine-side config, not in the shared .mcpx.json — the absolute-path leftovers in that file are #864’s to clean up. No new shared index location is created by this design.

8. Config surface

No new config fields, no new scope values. The entire config-visible change is the two placeholders and, in the final slice, flipping gopls-mcp-server to "scope": "worktree" in the fleet config. The user-level config stays what it is: a symlink to the repo .mcpx.json, with identical server lists on both machines — unchanged by this design.

9. Failure modes

  • Root vanished before spawn (checkout deleted between resolution and start): the spawn fails with the OS error surfaced as the instance start error. No silent fallback to the daemon cwd — a server running in the wrong workspace returns wrong answers, which is worse than a failed start.
  • Unreadable or partial git discovery: unchanged from today — locateGit degrades the key, the degradation is logged once, and the root follows the degraded key per §1.
  • Substitution errors: load-time for names (§4), spawn-time for an empty ${mcpx.root}; both name the server and the field.
  • Era-cache collisions: covered by §4’s identity rule; a regression here shows up as a server speaking the wrong era after a root change, which the slice-2 tests must pin down.

Non-goals

No new scopes; no daemon, service or deployment changes (how mcpx is installed or started on either machine is untouched); no per-project codedb or codebase-memory; no changes to sharing, leasing, idle or eviction policy.


What every affected issue has to change

  • dezren39/nix#817, #818, #819 — design source only from this record’s merge. Their acceptance criteria carry over verbatim into the implementation issues below. They stay open in nix until the #309 re-homing pass closes them with pointers to this record and to the implementation issues (closing them is #309’s acceptance criterion, not this record’s).
  • #309 — the re-homing list gains this record as the design home for the per-project trio; the trio’s implementation issues live here, so #309 should not re-file duplicates of them in this repo.
  • #308 — untouched: how downstream flakes consume mcpx is orthogonal to what a scope resolves to.
  • #864 — untouched, but slice 4 below depends on the fleet config being in its post-#864 shape (no /home/user absolute paths) or must say why it proceeded anyway.

Slices (implementation issues filed from this record)

  1. ResolvedScope plumbing + spawn cwd — §1, §2, §3, §6. (from #817)
  2. ${mcpx.root} / ${mcpx.cwd} substitution + era identity on the effective definition — §4. Depends on slice 1’s plumbing. (from #818)
  3. Initialize rootUri / workspaceFolders — §5. Depends on slice 1. (from #819)
  4. Fleet config flip + live proof — gopls-mcp-server to "scope": "worktree" in the fleet config; acceptance is the #819 live test: go_package_api on a module path returns the module’s API promptly through the daemon, from a caller in a linked worktree as well as the main checkout. Depends on slices 1 and 3; slice 2 not required.

Slices 1–3 each carry the acceptance criteria of their source issue, plus: tests for the spawn-cwd decision (scope root vs explicit cwd vs daemon fallback), for degraded roots, and for global sending nothing.

0005 — Release pipeline, version single-sourcing, and the module path

number:   0005
status:   proposed
date:     2026-10-29
issues:   #1881 (this design) · split from triage 2774 · implementation is queue task 3114
code:     7d1b081c

mcpx has no release at all: no tags, no GitHub releases, no release workflow, and a version string hard-coded in four places. The install page’s channels are promises nothing can keep until a release exists. This note decides the shape — channel order, the tag and version scheme for a package that lives in a monorepo, the static-build policy, and the module-path question — so the implementation slices have one answer to build to. The module-path recommendation (§4) is the one item Drew may want to overrule; everything else follows from the code as it is.


1. Current state (verified at 7d1b081c)

  • No tags and no releases on developing-today/code for mcpx; the only mcpx workflow builds and tests (mcpx.yml) plus vendorHash repair.
  • Version 0.1.0 is hard-coded four times: package.nix (version =), package.nix (-X main.version=0.1.0), the fallback var version = "0.1.0" in cmd/mcpx/main.go, and ARG VERSION=0.1.0 in Containerfile.
  • The module path was the last thing naming a repository that does not exist: slice 3 below renamed it to github.com/developing-today/code/pkgs/mcpx, where the code lives, and pointed meta.homepage at the real tree. (Before the rename, go.mod declared the legacy path and meta.homepage 404’d; the 328 Go files that imported by it moved with it in the same pass.)
  • A static build works: the SQLite driver is pure Go, CGO_ENABLED=0 cross-compiles, and a fully static Linux binary was verified in #182’s container work. The shipped artifacts are not static by choice: package.nix takes the nixpkgs cgo default and the Containerfile sets CGO_ENABLED=1 deliberately.
  • Unix only (meta.platforms = lib.platforms.unix; untagged Setpgid/Setsid use). Release targets are therefore the flake’s three systems: x86_64-linux, aarch64-linux, aarch64-darwin.
  • The nix package is more than the binary: it generates the man page and completions and wraps the binary with git/deno/bun/node on PATH. A bare release binary has no JavaScript runtime; mcpx run/exec fail until the user installs one (mcpx doctor says so). Every non-nix channel must state this in its install text.

2. Channel order

Build in dependency order; each channel consumes the one before it.

  1. GitHub release binaries + checksums. The foundation: three static binaries (mcpx-<version>-<system>.tar.gz) and a SHA256SUMS file on a GitHub release. Everything else downloads these.
  2. curl | sh installer. Per-user by default (installs to ~/.local/bin, states the JavaScript-runtime requirement at the end); a system-wide variant (sudo) documented, not default. The script verifies the checksum before installing — an installer that skips verification teaches users to pipe unverified binaries.
  3. Container image. The existing Containerfile, published to GHCR by the same release workflow, version taken from the release (§3), not from a fifth copy of the string.
  4. Nix. Already works (nix profile install …#mcpx); the release does not change it beyond the version source (§3).

Later, and not in the first slice: pwsh (needs Windows binaries, which need the untagged-syscall work), rpm/pkg (repackage the tarballs once they exist), the agent-prompt and just-the-plugin channels (they ride on 1–2). Flatpak is not built: a sandboxed mcpx cannot spawn the arbitrary servers, runtimes and binaries that are its entire function; the sandbox would have to be opened until the packaging is a lie. This records #1881’s recommendation against it as a decision.

3. Tags and the version’s single source

  • Tag scheme: mcpx/vX.Y.Z on developing-today/code. A path-prefixed tag is the monorepo-honest scheme: it cannot collide with another package’s releases in the same repository, and the release workflow triggers on exactly this prefix. (If §4’s recommendation is adopted, Go’s own subdirectory-module versioning wants pkgs/mcpx/vX.Y.Z tags for the module; the release workflow can cut both tags on the same commit — one for humans, one for the Go toolchain — rather than letting the Go scheme dictate the public one.)
  • Single source: a pkgs/mcpx/VERSION file. package.nix reads it for both its version and its -X main.version= ldflag; the Containerfile takes VERSION as a build argument whose workflow callers pass the file’s contents (the in-file ARG default becomes the file’s value at release time, and a comment in the Containerfile says the file is the source); the release workflow refuses to publish when the tag and VERSION disagree.
  • cmd/mcpx/main.go’s fallback becomes a sentinel (0.0.0-dev), so a binary built without ldflags announces that fact instead of impersonating a release. The fallback is the one place a literal remains, and it is deliberately not a release number.

4. The module path — options and recommendation

The module path used to name a repository that does not exist, so go install …@latest could not work and meta.homepage 404’d. Three ways out were recorded:

  • A — Create the repository the path names (dezren39/mcpx or developing-today/mcpx) and move mcpx into it. Cleanest end state: normal tags, normal go install, its own Pages and releases. Cost: mcpx leaves the monorepo it shares tooling, CI and history with; the move itself is a project, and this repository’s other packages that reference mcpx paths must follow. This is Drew’s call, not a worker’s.
  • B — Rename the module to where the code lives: github.com/developing-today/code/pkgs/mcpx. go install github.com/developing-today/code/pkgs/mcpx/cmd/mcpx@pkgs/mcpx/vX.Y.Z then works from the monorepo, tags follow Go’s subdirectory scheme, and meta.homepage points at the real tree. Cost: a mechanical rename across 328 Go files plus package.nix references — large diff, no design content, scriptable in one pass. If A is ever chosen later, the same mechanical rename runs again in the other direction; B does not close A’s door.
  • C — Keep the path, publish a release-only mirror. Releases are cut to a mirror repository that carries the path’s name. go install stays broken (the proxy resolves the module path, not the mirror’s releases), the homepage stays a lie, and two repositories must be kept in sync forever. Recorded for completeness; not recommended.

Decision: B, done in slice 3. It made every other part of this note true with one mechanical change and no new repository. A remains available whenever Drew wants mcpx to stand alone; nothing in B makes A harder.

5. Static-build policy for releases

Release binaries are CGO_ENABLED=0 static builds (the #182-verified configuration). The caveat is recorded where the install page will read it: on Linux, pure-Go os/user parses /etc/passwd instead of asking NSS, so LDAP/SSSD users are not found, and the pure-Go resolver does not honour nsswitch.conf. On macOS the flag changes nothing observable. The nix package keeps its own (cgo) build: it targets managed systems where NSS is the point, and it already exists.

6. Implementation slices (queue task 3114, in order)

  1. This design note.
  2. Version single-sourcing (§3): VERSION file, package.nix, Containerfile build-arg, the 0.0.0-dev fallback sentinel.
  3. Rename slice (§4-B), done: module path, imports, package.nix references, meta.homepage.
  4. Release workflow: tag-triggered (mcpx/v*) plus workflow_dispatch snapshot mode that builds the three artifacts and checksums without publishing; the snapshot run is the slice’s proof.
  5. Installer script + a CI exercise (install a snapshot artifact into a temp prefix; mcpx --version reports the release version).
  6. Container publication wiring in the same workflow; update the #1878 install-page status column to what actually shipped.

0006 — TTLs renew by touch-on-read, bounded by a maxAge ceiling

number:   0006
status:   accepted
date:     2026-10-29
issues:   #1884 · touches #309 #1887 #1893
code:     pkgs/mcpx at the merge commit of this record's PR

mcpx has at least seven TTLs across five subsystems and, until now, none of the absolute family (elicit, tasks, artifacts, ask, proto state) could be renewed: a question expired from its creation no matter how actively it was being used, while the idle family (daemon leases, MCP sessions, daemon idle-exit) already renewed by use — with no rule for which family a new resource joins. #1884 asked for one convention. This record is that convention.


Context

Verified at origin/main 7d1b081c, before this record’s PR:

  • internal/elicit/elicit.go — an elicitation’s ExpiresAt was set once at creation (Created.Add(defaults.ElicitTTL), 120s) and never moved. 120s was therefore the ceiling on how long a human could take to answer a question, however close they were to finishing.
  • internal/defaults/defaults.go — elicit.ttl existed only as a compiled-in default read from defaults.json; it was not a declared registry setting, so no config key, MCPX_* variable, flag, or /v1/settings could change it.
  • internal/daemon/server.go — the idle family already renews by use: trackActivity/shouldIdleExit keep a daemon alive while it is being used and let it die when it is not.
  • internal/artifacts/artifacts.go — artifacts expire absolutely at put (Expires: now.Add(ttl)).

Options

A — A /renew endpoint or per-resource renew operation

Every resource grows a renewal verb. Rejected in #1884: it puts the burden on every caller forever, and a caller that forgets is exactly the case renewal exists for.

B — Touch-on-read, unbounded

Any read renews forever. Rejected: a resource somebody keeps glancing at becomes immortal, and expiry stops meaning anything.

C — Touch-on-read with an absolute ceiling (chosen)

Reading a resource renews its idle deadline; no renewal can push the deadline past creation + maxAge. An explicit lifetime given at creation is the one exception: creation is where a lifetime is chosen, reads only renew within the convention.

Decision

  1. Touch-on-read is the default. An item read of a pending resource sets lastTouched = now and the effective deadline to min(created + maxAge, lastTouched + ttl). Collection reads do not touch: listing what exists must not keep it alive.
  2. Touch never shortens and never revives. A deadline set explicitly at creation stands; a resource whose deadline has passed is expired, not renewed by a late read.
  3. One vocabulary. Every expiring representation carries expiresAt (RFC 3339), lastTouched, and ttlRemainingMs (milliseconds until expiresAt when the representation was produced, never negative).
  4. No /renew endpoint or per-resource renew operation is added, anywhere, for any family.
  5. The knobs are declared settings. elicit.ttl and elicit.maxAge are registry settings (config key, MCPX_ELICIT_TTL / MCPX_ELICIT_MAX_AGE, flags, /v1/settings), defaulting to 120s and 30m.
  6. Internal reads do not touch. Machinery that polls a resource to enforce its deadline (the broker’s Await, Respond’s validation) reads without touching; a read that renewed the deadline it enforces would never let it fall.

First family implemented: elicit (this record’s PR). The remaining absolute families adopt the same convention in follow-ups named on #1884: tasks, ask, artifacts (absolute by default, opt-in artifacts.idleTtl). proto state is the documented exception: its expiry is signed into the token, so it cannot be touched; it is renewed by re-minting.

Consequences

  • #1884: implemented for the elicit family by this record’s PR; remaining families split into linked follow-ups, named on the issue, none silently dropped.
  • Forbidden: any /renew verb; any new expiring resource that does not state which side of this convention it is on.
  • Deferred: tasks/ask/artifacts adoption (follow-ups on #1884); security/auth questions (the issue’s tracker, #1887/#1893).

Mapping

termfrombecomes
renew endpoint#1884 option Aforbidden — touch-on-read instead
absolute expiryelicit/tasks/artifactsidle deadline + maxAge ceiling (per family PR)
elicit.ttlcompiled-in defaultdeclared registry setting
stateTTL renewalproto statere-minting the token (documented exception)

The home-page example component

Issue #1876, Approach C, stages 1–2. This directory is the example slot of the mcpx home page, landed in the docs source because the page itself (#1874) and the site build/deploy (#1941) have not landed yet — when they do, the generator includes component.html where the example goes and runs generate-client.sh as part of the build. Nothing here is a parallel site.

What a reader gets

  • Stage 1 — copy-as-command (copy-command.js). Every docs example can be copied two ways: Copy as mcpx exec (a quoted-heredoc command that runs the snippet against the reader’s real daemon; see the file header for why the quoting has to be a heredoc) and Copy as script (the source itself, plus scriptCommands() for the save-and-mcpx run two-liner).
  • Stage 2 — run it in the page (component.html, run-example.js). The example is editable; Run executes it in the tab. The label says “Simulated — runs in your browser”, because it is one:
    • the script runs through a strip-only TypeScript transform (strip-types.js) and is imported as a blob module;
    • it talks to the real generated client — client.ts, emitted by mcpx client --ns demo against the deterministic fixture server (internal/testsupport/fakemcp), and client.browser.js, that same file after Node’s strip-only transform. Neither is hand-written; regenerate both with ./generate-client.sh;
    • the client’s one hot route, POST /v1/call, is answered by a fake daemon (fake-daemon.js) from in-page mocks (mocks.js). /v1/batch, /v1/resource and /v1/artifacts are not simulated and answer with an honest error.
  • No-JS fallback: the static highlighted example, unchanged.

Fidelity gaps (also stated on the page)

  • The only namespace is the mock demo fixture; its deterministic tools are mirrored, the rest return an error naming the limitation. No real MCP servers, no launcher placeholders, no Deno.*/node:fs.
  • Mocks are computed from the same rules as the fixture where the fixture is deterministic (echo, open/state/wipe, structured, boom, fancy-name).

Stage 3 is not here

Running against the reader’s own daemon (a daemon.cors allow-list setting, a fixed daemon.port, Chrome Local Network Access handling) is excluded — a separate later follow-on. A hosted public daemon is rejected in the issue, permanently. This component makes no daemon changes.

Verify

node smoke-test.mjs   # shell round-trip for stage 1; end-to-end run for stage 2
./generate-client.sh  # regenerate client.ts / client.browser.js from the fixture

#1941 should wire smoke-test.mjs into the docs CI pipeline; until then it is the recorded smoke check for this component.

docs/spec

created:      2026-09-30T12:00:00-05:00
last-updated: 2026-09-30T12:00:00-05:00
increment:    1
status:       standard
tags:         area:protocol, area:spec
description:  an index of the protocol records: what the specification asks,
              what mcpx does on the wire, and how that is measured.

../protocol.md is the design, and where to start. These are the records under it, each tested by name (<revision>/<area>/<requirement>).

What the specification asks, independent of mcpx:

fileone line
server-obligations.mdper revision, what a server MUST, SHOULD and MAY do, including what the prose implies and never states
client-obligations.mdthe same for a client, and what a proxy owes on top

What mcpx does, as a server and as a client:

fileone line
transport.mdmcpx as a server on the wire: stdio and Streamable HTTP, per revision, headers, statuses, sessions, Origin
messages.mdwhat mcpx sends per revision: the 2026 envelope, per-revision fields, listen, not-found, tasks, requestState
identity.mdwho an MCP request is from, per connection, and what scope that gives it
client.mdmcpx as a client: headers, x-mcp-header, results, lifecycle, listen, elicitation, resumption, HTTP+SSE
era-probe.mdhow mcpx decides an upstream’s era, and the cache that makes it a one-time cost

How it is measured:

fileone line
requirements.mdthe catalogue: every normative requirement of every revision, one row each, with its quote and anchor
revision-conflicts.mdthe rules that change between revisions, and where one revision contradicts itself
conformance.mdhow the matrix is built from the catalogue, the test covers and the gaps, and what building it found
conformance-matrix.mdgenerated: every requirement, per revision and side, with the test that verifies it or the issue that tracks the gap
official-suite.mdthe official modelcontextprotocol/conformance suite against mcpx, and which failures are defects

Not here: how mcpx compares with the revisions side by side, with opencode, lootbox and Cloudflare code mode, and every place two of them disagree, is ../compare/ — start with matrix-revisions.md and conflicts.md.

What the specification asks of a client

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T16:00:00-05:00
increment:    2
status:       standard
tags:         area:protocol, area:spec
description:  per revision, what an MCP client MUST, SHOULD and MAY do,
              including the obligations the prose implies but never states.

This is the counterpart to server-obligations.md and follows the same conventions. It is about the specification. mcpx is a client of every upstream server it runs, and its client behaviour is in ../protocol.md §4. Comparisons with other clients belong in ../compare/.

“Legacy” means 2024-11-05 through 2025-11-25, and “modern” means 2026-07-28. Several obligations below attach to the host, the application that shows things to a user. The spec addresses these to clients. A proxy is a client with no user of its own, so for each of them it has to decide whether it meets the obligation itself or passes it downstream.


1. Lifecycle

Sources: 2025-11-25 lifecycle, 2026-07-28 basic, 2026-07-28 versioning.

Legacy

  • initialize comes first. A client MUST send initialize before anything else, offering the latest version it supports.
  • Check the answer. If the server answers with a version the client cannot speak, the client SHOULD disconnect. The schema’s doc comment says MUST. A client that accepts whatever comes back is running a protocol it never agreed to.
  • Then notifications/initialized. After a successful result the client MUST send notifications/initialized. Before the server responds it SHOULD NOT send requests other than pings.
  • Declare what it can do. The client declares roots, sampling and, from 2025-06-18, elicitation. What it declares is what the server may ask of it.
  • Never cancel initialize. notifications/cancelled for initialize is forbidden in every legacy revision, including after the request times out. The client closes the connection instead.
  • stdio shutdown. Close stdin, wait, then SIGTERM, then SIGKILL. In 2026 this rule moved to the stdio transport page.

Modern (2026-07-28)

  • Every request carries _meta. Every request MUST carry _meta["io.modelcontextprotocol/protocolVersion"] and clientCapabilities. The client capabilities apply to that request only. An empty object means none.
  • clientInfo. A client SHOULD send clientInfo.
  • resultType. An absent resultType means "complete". An unknown value MUST be treated as invalid, never as complete.
  • -32022 (unsupported version). The client SHOULD pick a version from data.supported and retry.

Declaring capabilities per request has an effect that shows up in multiplexing clients. The client can widen or narrow what it offers one call at a time. For example, it can declare elicitation only on calls made while a user is present. The server cannot hold it to an earlier declaration.


2. Finding the era

Source: 2026-07-28 versioning, backward compatibility.

The 2026 versioning page describes the probe. The result is “a property of the server”. A client SHOULD cache it for the life of the process (stdio) or the origin (HTTP), and MAY persist it across restarts of the same configuration.

stdio. Send server/discover first (SHOULD). If the answer is anything other than a recognised modern result or modern error, fall back to initialize, and that includes a timeout. The fallback MUST NOT be keyed to one error code, because legacy servers answer an unknown method in different ways.

HTTP. A dual-era client tries the steps in order:

  1. POST a modern request with every header. Success means the server is modern.
  2. A 400, 404 or 405 whose body is a JSON-RPC error with -32020, -32021, -32022, or -32601 together with 404, also means modern. Fix the request or pick a version, and retry.
  3. Anything else: POST initialize, which gives legacy Streamable HTTP.
  4. If that fails with 400, 404 or 405 (the 2025-11-25 rule), GET the URL and wait for an endpoint event, which gives HTTP+SSE (2024-11-05).

A server that returns a bare-text 400 is therefore classified as legacy. That is the server’s fault, but the client can only act on what it received.

A pool of processes of one configuration shares one era answer. The cache key is the configuration, not the process. A freshly spawned legacy process still needs its own initialize, because the session is per process. A modern one needs nothing. If a pooled process answers a different era from its siblings, the binary changed, and the cached answer has to be dropped and the probe run again.


3. Transports

Sources: 2024-11-05 transports, 2025-11-25 transports, 2026-07-28 stdio, 2026-07-28 Streamable HTTP, 2025-11-25 authorization.

stdio

  • The client launches the server as a subprocess.
  • It MUST NOT write anything to stdin that is not an MCP message.
  • It SHOULD NOT treat stderr output as an error (2025-11-25 and later).
  • In 2026 it SHOULD restart a server that exited and retry.

HTTP+SSE (2024-11-05)

  • The client opens the SSE stream first and POSTs to the URL from the endpoint event.
  • A backwards-compatible client uses this transport only as the last fallback (§2).

Streamable HTTP, legacy (2025-03-26 .. 2025-11-25)

  • Accept header. POST with Accept: application/json, text/event-stream, and handle both kinds of response.
  • Session id. Echo Mcp-Session-Id on every request once it has been assigned. On a 404 for a request that carried a session id, MUST start a new session with initialize. The client SHOULD DELETE the session when it is done.
  • Version header (2025-06-18 and later). Send MCP-Protocol-Version: <negotiated version> on every request after initialize. This is the version the server answered, not the one the client prefers. A constant header is wrong whenever negotiation settled lower.
  • Disconnects. A disconnect is not a cancellation. To cancel, send notifications/cancelled on a new POST.
  • Server-initiated messages. The client MAY open a GET stream for them, and MAY resume with Last-Event-ID.

Streamable HTTP, modern (2026-07-28)

  • Headers. Send MCP-Protocol-Version from the request’s own _meta, plus Mcp-Method, Mcp-Name, and Mcp-Param-* for every tool parameter the server annotated with x-mcp-header.
    • A tool whose x-mcp-header annotations are invalid MUST be excluded from the client’s tool list.
    • A proxy that re-exports such a tool takes on both sides: it sends these headers upstream, and it validates them from its own clients.
  • No sessions or background streams. There is no session, no GET stream, and no resumption. A broken request is re-issued under a new id. That MUST appears only in the 2026 changelog.
  • Cancellation. Closing the stream is cancellation.

Authorization (HTTP, 2025-03-26 and later)

A client that authenticates:

  • follows the OAuth flow the server’s metadata describes (PKCE is REQUIRED);
  • sends Authorization: Bearer on every request, never as a URL query parameter;
  • uses a token only for the resource it was issued for (the resource parameter from 2025-06-18 on).

On stdio the client passes credentials through the environment. A client that sends static configured headers meets “never in the query” only if none of its auth modes puts a token in the URL. mcpx’s query auth type does, and since #240 an auth block is applied, so query sends one when declared.


4. Messages and utilities

Sources: 2025-11-25 utilities (cancellation, progress, ping), 2026-07-28 cancellation, subscriptions, caching, tasks extension.

  • Ids.
    • Request ids are unique within a session (legacy).
    • A client answering server-to-client requests keeps its own id space for them.
    • A proxy that re-issues upstream questions downstream must never let the two id spaces meet.
  • Timeouts.
    • A client SHOULD set a timeout on every request, and SHOULD cancel upstream when one fires.
    • It MAY reset the timer on progress, but SHOULD still enforce a maximum.
    • A proxy sits between two timeouts. Its upstream timeout has to be shorter than its downstream client’s, or the client gives up first and the upstream work runs on with no observer.
    • A subscriptions/listen request is an implied exception, because it normally never gets a response.
  • Cancellation. A client MUST NOT cancel initialize. After it cancels a request it ignores any response that still arrives.
  • Progress. A progressToken MUST be unique among active requests. In 2025-11-25, a token given to a task stays valid until the task is terminal. That can be longer than the connection that carried the original request.
  • Pagination.
    • Cursors are opaque. The client MUST NOT parse or persist them across sessions.
    • In 2026 an empty-string nextCursor is not the end of the list. Only an absent nextCursor ends it.
  • Ping (legacy). A client MUST answer the server’s pings. 2026 has no ping, so the health probe for a modern upstream is a cheap real request, and server/discover is cacheable and made for it.
  • Logging.
    • Legacy: logging/setLevel is per session.
    • 2026: the level is per request, in _meta. A client that wants logs asks on each call.
  • Caching (2026).
    • A client honours ttlMs and cacheScope.
    • It MUST NOT share a private result across authorization contexts.
    • Every page of one list carries the same cacheScope.
    • An intermediary cache is bound by the same rules.
  • Subscriptions (2026).
    • list_changed and resources/updated arrive only on a subscriptions/listen stream whose filter asked for them. A client that never listens never hears about changes.
    • A client SHOULD check the filter in the acknowledgement against the one it requested.
    • A client may open several listens, and tells them apart by _meta.subscriptionId, which equals the listen request’s id.
    • Teardown arrives as a result or as a stdio notifications/cancelled. The spec disagrees with itself on which (conflicts C-21), so a client accepts both.
  • Tasks.
    • A client that creates a task SHOULD store the task id durably. The handle outlives the request that created it and the connection it was created on.
    • The client polls with tasks/get, at no faster than pollInterval.
    • With the 2026 extension, a client receives a task only if it declared the extension. It has to handle resultType: "task" on any request where it declared the extension.

5. Features the client provides

Sources: 2025-11-25 roots, sampling, elicitation; 2026-07-28 MRTR.

Roots

  • Declaring. Declare roots, with listChanged only if the client will send notifications/roots/list_changed.
  • Roots themselves. Roots MUST be file:// URIs. The client SHOULD validate them against path traversal and ask the user before exposing them.
  • 2026. Roots are requested through MRTR only. There is no change signal and no session, so a server caches roots per request, or per requestState.

Sampling

  • Human in the loop. A client SHOULD let a human review and edit the request and the completion, and deny either. A host that auto-approves makes that choice for the user and should say so.
  • Model preferences. modelPreferences are advisory, and hints are matched in order.
  • Tools and context (2025-11-25 and later).
    • Declaring sampling.tools promises to run tool loops. A client must not declare it unless every path that can answer supports them.
    • A server MUST NOT send tools or toolChoice to a client that did not declare sampling.tools.
    • includeContext other than "none" is soft-deprecated, and needs sampling.context.
  • 2026.
    • Sampling is deprecated, but it is fully functional during the deprecation window.
    • The request travels through MRTR.
    • The client SHOULD NOT retain sampling messages between requests.

Elicitation (2025-06-18 and later)

  • Form mode.
    • An empty elicitation: {} means form mode only.
    • The requested schema is flat, made of primitives only.
    • The client MUST let the user review and change a response before it is sent, and can decline or cancel.
    • The client MUST show which server is asking.
    • It MUST NOT ask for sensitive information through a form (2025-11-25 and later).
  • URL mode (2025-11-25 and later) requires elicitation.url.
    • A client that did not declare it and receives a URL request MUST answer -32602.
    • A client that did declare it MUST show the full URL with the domain highlighted, MUST NOT open it without explicit consent, and MUST NOT prefetch it.
    • URL mode exists precisely so the client never sees the secret.
    • -32042 and notifications/elicitation/complete exist only in 2025-11-25. The notification goes only to the client that received the elicitation.
  • Binding the answer to a user. Elicitation state MUST NOT be bound to session ids alone (2025-11-25), so the elicited authorization has to reach the user who started the call.
  • 2026.
    • Elicitation travels through MRTR only.
    • elicitationId, the completion notification, -32042, the session-id rule and client rate limiting are all gone.

What a proxy owes, in addition

A proxy answers these requests for upstream servers by asking its own clients. The spec does not address proxies here, but the rules above force the following:

  • Check the capability per hop. Before forwarding a question downstream, the proxy checks that that downstream declared the capability, on the request being answered (2026), and down to the mode: elicitation.url, sampling.tools and sampling.context each need their own check.
  • Scope includeContext. Through a proxy, "thisServer" means every aggregated server. The proxy forwards "none" unless it can scope the context to the originating upstream.
  • Translate between eras.
    • Legacy upstream to modern downstream: keep the upstream’s wire request open while the downstream receives input_required.
    • Modern upstream to legacy downstream: ask each input request on the wire, then retry upstream with the upstream’s own requestState, byte for byte.
    • Never relay -32042 to a 2026 client. Convert it into a URL-mode input request.
    • Strip elicitationId for 2026 clients, and synthesise one for 2025-11-25 clients.
  • Always answer upstream. If a downstream never retries, a legacy upstream request stays pending forever. The proxy has to answer it with an error or cancel.
  • Attribute the question and leave it intact. Name the originating upstream. Leave url, requestedSchema, hint order and maxTokens untouched.
  • Pooling breaks user binding. An elicited third-party authorization is bound to the proxy’s client identity. With a shared pooled upstream, one user’s URL completion becomes visible to another user’s calls. Per-user scoping is the only remedy.

6. Where mcpx stands

What mcpx does as a client is client.md and era-probe.md, summarised in ../protocol.md §4; the per-requirement status, each row with the test that verifies it, is conformance-matrix.md. Of the gaps the first draft of this page listed, the discover field names, the unchecked initialize answer, the -32022 retry, the unknown-resultType fallback, the constant version header, the missing Mcp-Method/Mcp-Name/Mcp-Param-* headers, the HTTP+SSE fallback, the cancelled initialize, paging that stopped at an empty cursor, undeclared url-mode elicitation and elicitationId reaching 2026 clients were fixed by #224, #232 and #239. What the rules above still expose:

  • No OAuth (#253). The per-server auth block is applied (#240); type: oauth is refused before connecting.
  • sampling and elicitation.url are declared whenever a handler is installed, and the daemon always installs one; roots is declared and always answered with an empty list (#210).
  • sampling.tools and sampling.context are not checked per hop when a question is forwarded (#210, #256).
  • No progressToken is sent upstream, so progress cannot extend a timeout (#212).
  • A legacy HTTP session answered 404 is an error, not a reason to send initialize again (#203). (stdio shutdown now closes stdin, waits, then SIGTERM, then SIGKILL, as the lifecycle page asks — #284.)

mcpx as a client: every revision

What mcpx sends to the servers it fronts, and what it accepts from them, revision by revision. The server side is transport.md; how the era is found is era-probe.md. Everything here is tested; test names are <revision>/<area>/<requirement> in internal/mcpclient/{conformance_client,http_conformance,http_version}_test.go, internal/pool/sse_test.go and internal/e2e/client_test.go. Every frame the scripted servers receive is checked against the schema of the revision in use, in strict mode (mcpspec.ValidateClientMessageStrict): a key another revision defines is a failure, because “send conservatively” is exactly the rule a lenient check cannot see.

mcpx as a client speaks 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 (legacy, initialize) and 2026-07-28 (modern, per-request _meta).

Headers

  • Legacy Streamable HTTP. MCP-Protocol-Version is the version initialize returned, on every request after it. It used to be the constant 2025-11-25, so a server that negotiated down to 2025-06-18 was told on every request that the client was speaking a revision it had just declined. initialize itself carries no header (nothing is agreed yet), and a server that negotiated 2025-03-26 or earlier gets none: the header did not exist before 2025-06-18.
  • Modern Streamable HTTP. The header is the version in the frame’s _meta; Mcp-Method is on every POST; Mcp-Name on tools/call, prompts/get and resources/read. Mcp-Session-Id is never sent on a modern frame — there are no sessions in 2026-07-28, and one minted for an earlier legacy exchange means nothing to a modern request. mcpx’s own daemon rejects a modern POST without the mirrored headers, which is why TestMcpxIsAModernClientOfMcpx (one mcpx’s /mcp as another’s upstream) is the real proof rather than a fake written to agree.
  • Value encoding. A value outside visible ASCII, space and tab, with leading or trailing whitespace, or one that already looks like the sentinel (=?base64?…?=), is sent as the sentinel around standard Base64. The last case matters: a plain =?base64?literal?= would otherwise be decoded by the server into something the client never sent.

x-mcp-header

A modern server over HTTP may annotate tool parameters with x-mcp-header; the client MUST mirror them into Mcp-Param-{Name} headers.

  • Validation, at tools/list. An annotation must be a non-empty RFC 9110 token, case-insensitively unique, on an integer, string or boolean (never number), reachable from the root through properties alone. The walk visits the whole schema, so an annotation under items, anyOf, not, $defs or at the root is found and makes the tool invalid rather than being silently ignored. An invalid tool is excluded from the list mcpx returns and a server.warning lifecycle event names it and the reason. Only on a modern HTTP connection: stdio clients MAY ignore annotations, and legacy revisions never defined them.
  • Mirroring, at tools/call. The value at the annotated path; absent or null omits the header; an empty string is sent as an empty header (the value was provided). Integers outside ±(2^53−1) and non-integers for an integer header are refused before sending — a header the server will reject is worse than no call. A connection that has not listed tools lists them once before its first call, since the annotations come from the list.
  • -32020 on a call means the schema most likely changed since it was listed: mcpx reads tools/list again and retries once (the transport page’s SHOULD).

Results and errors

  • resultType. Absent is complete. complete and input_required are understood; anything else is an InvalidResultError. It used to be treated as complete, so a result meaning something mcpx does not know reached a caller as if it were a finished answer.
  • Resource not found is ResourceNotFoundError: -32002 in every revision, and -32602 from a modern server (2026-07-28 uses -32602 and asks clients to accept -32002 too). A legacy -32602 stays invalid-params: in those revisions it means the request was wrong, not that the resource is missing.
  • Pagination. A missing nextCursor ends a list in every revision. In 2026-07-28 an empty string is a cursor (“don’t make any determination based on cursor value other than whether a non-null value was provided”), so mcpx asks again with "". The legacy pages never said so, and a legacy server that writes an unset cursor as "" means the end, so there "" ends the list. A cursor that repeats the one just sent also ends it.
  • Version retry. A -32022 to server/discover is retried with a version from data.supported — first one not yet tried, then, once, one already tried that the server nonetheless lists. The official suite rejects the first request with supported naming the very version it rejected; retrying it once is what “select a mutually supported version and retry” means there. Each version is sent at most upstream.versionAttempts (2) times, so a server that keeps doing it cannot loop the probe.

Lifecycle and cancellation

  • Negotiated version check. If initialize returns a version mcpx does not speak, mcpx disconnects with UnsupportedVersionError and does not send notifications/initialized. Carrying on meant speaking a revision nobody agreed to.
  • initialize is never cancelled. Every legacy cancellation page forbids it; a timed-out initialize used to send notifications/cancelled.
  • Modern HTTP: closing the response stream is the cancellation; no notifications/cancelled is POSTed. Modern stdio: the notification MUST be sent, and is. Legacy: sent – including for an HTTP request timed out while its POST still waited for response headers, which was never cancelled: Send lasts until the headers arrive, so that timeout surfaced as a send error, before the code that sends notifications/cancelled.
  • ping and logging/setLevel are not sent to a modern server — 2026-07-28 removed both. Ping becomes server/discover, which every modern server MUST implement and which likewise only answers. A log level becomes io.modelcontextprotocol/logLevel in every later request’s _meta (the daemon asks for info); without it a modern server MUST NOT log at all.
  • A modern response stream that closes without its response is answered at once with an error (-32000), where the caller used to wait for its deadline: 2026-07-28 has no resumption, and “a broken response stream loses the in-flight request”.
  • Capabilities. roots is {} on a modern request (2026-07-28 dropped listChanged with notifications/roots/list_changed), {"listChanged": false} in initialize.

Notifications from a modern server: subscriptions/listen

A modern server delivers list changes and resource updates only on a subscriptions/listen stream. mcpx opens one per modern connection, asking only for what the server declared (tools|prompts|resources.listChanged, and resourceSubscriptions when resources.subscribe is declared and something is subscribed). SubscribeResource on a modern connection adds the URI and replaces the stream: the old one is ended the way the transport requires (closing it over HTTP, notifications/cancelled naming the listen id on stdio) and a new one opened with the new filter.

Notifications are correlated by _meta["io.modelcontextprotocol/subscriptionId"] — the stdio MUST. One tagged with the id of a stream mcpx has since replaced is dropped; untagged ones (progress, log lines, which belong to a request) pass. The acknowledgement’s filter is compared with what was asked and anything declined is reported as a server.warning. When the server ends the stream — gracefully with a response or not — it is reopened after upstream.listenReopenDelay.

The spec disagrees with itself on how a server ends one (subscriptions page: a successful response; cancellation page: notifications/cancelled naming the listen id). mcpx as a client accepts both: a response ends the pending request, and a cancelled notification is harmless.

Resource updates forwarded to mcpx’s own clients now carry the mcpx:// URI the client subscribed with; they carried the upstream URI, which matches no subscription the client holds. The match is on the upstream URI, because the event names a server and the listing names a namespace that a config may override and mcpx serve cannot see.

Server requests and elicitation

  • Ids. A server’s request id is kept raw: a string id is as valid as a number, and one was dropped as unparseable.
  • Batches. A 2025-03-26 server may send a JSON array; each element is now dispatched. On stdio the array arrived as one line starting with [ and was dropped whole.
  • Declarations. elicitation.form always (mcpx answers cancel when nobody is listening, which is truthful); elicitation.url only with a handler installed — the broker stores the URL, shows it (mcpx elicit list), takes the answer and honours notifications/elicitation/complete, so it is carried end to end; sampling only with a handler. A request in a mode that was not declared for the revision in use is -32602, as the elicitation page requires: url mode to a server that negotiated 2025-06-18 is refused even though initialize offered it, because that revision has no url mode.
  • Defaults. An accepted form answer’s missing fields are filled from the requested schema’s defaults (“clients that support defaults SHOULD pre-populate form fields”); a field the answerer set is never overwritten.
  • The hang the official suite found. An elicitation/create sent during a tools/call over Streamable HTTP never reached the broker. The TypeScript SDK’s server.request() inside a tool handler is not related to the POST, so it goes to the standalone GET stream — which mcpx never opened. mcpx now opens it after initialize on any Streamable HTTP server that negotiated 2025-03-26 or later; a server without one answers 405 and that is the end of it.

Resumption (2025-11-25)

A POST’s SSE stream that the server closes before the response is resumed: wait the retry the server sent (or upstream.sseReconnectDelay), then GET with Last-Event-ID, up to upstream.sseReconnectAttempts times. A stream with no event id cannot be resumed and the caller is told at once. The standalone GET stream reconnects the same way.

HTTP+SSE (2024-11-05)

When a Streamable HTTP endpoint refuses both the modern probe and initialize with a bare 400, 404 or 405 (LegacyHTTPRefusedError), the pool GETs the URL expecting an endpoint event, POSTs every message there and reads replies from the stream. The endpoint must be on the stream’s origin: it is where every message — and the configured credential headers — goes, and a server able to name any URL could otherwise have mcpx post credentials anywhere. The result is cached like any era, with "transport": "http+sse" in the era record, so the next start goes straight there without the two refused POSTs.

Official suite, client leg (before → after)

setbeforeafter
--requirements 2025-11-255 passed, 64 failed20 passed, 56 failed
--requirements 2026-07-2823 passed, 101 failed, 1 warning63 passed, 68 failed, 0 warnings

Every remaining failure is auth/*: mcpx has no OAuth client. One non-auth scenario, json-schema-ref-no-deref, is flaky because of the adapter, not mcpx; official-suite.md has the cause and the current numbers. The json-schema-2020-12-preservation pass needed the adapter (internal/conformance/officialclient) to echo the schema back as mcpx holds it; it used to call every tool with {}.

Not done

  • OAuth. The auth config key is applied to every upstream connection (internal/pool resolveAuth, #240): bearer, basic and header become headers, env becomes the child’s environment, query is added to the URL. type: oauth is refused before connecting. The spec forbids access tokens in a query string; query exists for services that require it and sends one only when you declare it.

MCP conformance matrix

Generated by internal/conformance (MCPX_UPDATE_MATRIX=1 go test ./internal/conformance -run TestMatrix); do not edit.

Generated from inputs 247a9508b4eda6e7 (sha256 of docs/spec/requirements.md, internal/conformance/overrides/.json, internal/conformance/coverage.go, internal/conformance/authsplit.json). TestMatrix regenerates this file and fails when it differs, so it describes the tree it is committed in.

Every normative requirement of every revision (catalogue: docs/spec/requirements.md), for mcpx as a server and as a client. A requirement is tested when a named test verifies it in that revision, n/a when it does not bind mcpx in that role, and a gap when it binds mcpx and mcpx does not meet it yet, with the issue that tracks it. Run exactly the covering tests with go run ./internal/conformance/cmd/covering.

Summary

revisionsidelevelappliestestedgapmissingn/a
2024-11-05serverMUST32311022
2024-11-05serverMUST NOT66004
2024-11-05serverSHOULD36351045
2024-11-05serverSHOULD NOT11001
2024-11-05serverMAY990015
2024-11-05serverother1111001
2024-11-05clientMUST26251028
2024-11-05clientMUST NOT99001
2024-11-05clientSHOULD31265050
2024-11-05clientSHOULD NOT11001
2024-11-05clientMAY990015
2024-11-05clientother77005
2025-03-26serverMUST44431037
2025-03-26serverMUST NOT77008
2025-03-26serverSHOULD45441058
2025-03-26serverSHOULD NOT43101
2025-03-26serverMAY16160021
2025-03-26serverother1111001
2025-03-26clientMUST35314046
2025-03-26clientMUST NOT1211103
2025-03-26clientSHOULD39336064
2025-03-26clientSHOULD NOT21103
2025-03-26clientMAY15150022
2025-03-26clientother77005
2025-06-18serverMUST52511049
2025-06-18serverMUST NOT109108
2025-06-18serverSHOULD48471067
2025-06-18serverSHOULD NOT43101
2025-06-18serverMAY15150020
2025-06-18serverother1515003
2025-06-18clientMUST42375059
2025-06-18clientMUST NOT1311205
2025-06-18clientSHOULD48408067
2025-06-18clientSHOULD NOT21103
2025-06-18clientMAY13130022
2025-06-18clientother108208
2025-11-25serverMUST102984078
2025-11-25serverMUST NOT22193013
2025-11-25serverSHOULD73658086
2025-11-25serverSHOULD NOT1110103
2025-11-25serverMAY25250030
2025-11-25serverother1919002
2025-11-25clientMUST655780115
2025-11-25clientMUST NOT16142019
2025-11-25clientSHOULD5848100101
2025-11-25clientSHOULD NOT54109
2025-11-25clientMAY17170038
2025-11-25clientother1311208
2026-07-28serverMUST11310940111
2026-07-28serverMUST NOT44422020
2026-07-28serverSHOULD69654091
2026-07-28serverSHOULD NOT1110109
2026-07-28serverMAY29290032
2026-07-28serverother2322104
2026-07-28clientMUST736580151
2026-07-28clientMUST NOT25232039
2026-07-28clientSHOULD66579094
2026-07-28clientSHOULD NOT1311207
2026-07-28clientMAY11110050
2026-07-28clientother13112014

2024-11-05

(security; not in area list)

requirementlevelsidemcpx as servermcpx as client
security-implementors-guidelinesSHOULDbothtested: conformance.TestMessagesServer/2024-11-05/security/security-implementors-guidelinesn/a: catalogue marks it n/a

authorization

requirementlevelsidemcpx as servermcpx as client
authorization-custom-auth-mayMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a

cancellation

requirementlevelsidemcpx as servermcpx as client
cancellation-ref-same-direction-in-progressMUSTbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-ref-same-direction-in-progresstested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-ref-same-direction-in-progress
cancellation-client-must-not-cancel-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-client-must-not-cancel-initialize
cancellation-receiver-should-stop-free-no-responseSHOULDbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-receiver-should-stop-free-no-responsetested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-receiver-should-stop-free-no-response
cancellation-receiver-may-ignoreMAYbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-receiver-may-ignoretested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-receiver-may-ignore
cancellation-sender-ignore-late-responseSHOULDbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-sender-ignore-late-responsetested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-sender-ignore-late-response
cancellation-handle-racesMUSTbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-handle-racestested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-handle-races
cancellation-log-reasonsSHOULDbothgap #77: a cancellation reason reaches Server.OnCancel, which nothing outside tests sets; it is never loggedtested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-log-reasons
cancellation-ui-indicateSHOULDbothn/a: no UIn/a: no UI
cancellation-ignore-invalidSHOULDbothtested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-ignore-invalidtested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-ignore-invalid
cancellation-processing-should-ceaseSHOULDboth (receiver)tested: conformance.TestUtilitiesServer/2024-11-05/cancellation/cancellation-processing-should-ceasetested: conformance.TestUtilitiesClient/2024-11-05/cancellation/cancellation-processing-should-cease

completion

requirementlevelsidemcpx as servermcpx as client
completion-max-100descr. + schemaservertested: conformance.TestUtilitiesServer/2024-11-05/completion/completion-max-100n/a: addresses the other side
completion-server-shouldSHOULDservertested: daemon.TestHandleCompleteRateLimitHotChangeAndZeroDisables@all
daemon.TestHandleCompleteRateLimitRefusesBeforePool@all
daemon.TestHandleCompleteServerWindowBindsAcrossCallers@all
daemon.TestLocalCompletionCapAfterRanking@all
daemon.TestLocalCompletionTiersPrompts@all
daemon.TestLocalCompletionTiersResources@all
n/a: addresses the other side
completion-client-shouldSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/completion/completion-client-should
completion-securityMUSTbothgap #1968: partial: completion inputs are validated and completion/complete is rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) since #2387, but there is no per-caller access control for sensitive suggestions and no completion-specific information-disclosure control; waiting on principals #1968 / permissive-auth #1972n/a: catalogue marks it n/a

errors

requirementlevelsidemcpx as servermcpx as client
errors-error-has-code-and-messageMUSTbothtested: conformance.TestMessagesServer/2024-11-05/errors/errors-error-has-code-and-messagetested: conformance.TestMessagesClient/2024-11-05/errors/errors-error-has-code-and-message
errors-codes-integersMUSTbothtested: conformance.TestMessagesServer/2024-11-05/errors/errors-codes-integerstested: conformance.TestMessagesClient/2024-11-05/errors/errors-codes-integers
errors-message-concise-single-sentenceSHOULDbothtested: conformance.TestMessagesServer/2024-11-05/errors/errors-message-concise-single-sentencen/a: sends no errors except to server requests

lifecycle

requirementlevelsidemcpx as servermcpx as client
lifecycle-implementations-support-base-and-lifecycleMUSTbothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-implementations-support-base-and-lifecycletested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-implementations-support-base-and-lifecycle
lifecycle-initialization-first-interactionMUSTbothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-initialization-first-interactiontested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-initialization-first-interaction
lifecycle-client-sends-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-client-sends-initialize
lifecycle-server-responds-caps-and-infoMUSTservertested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-server-responds-caps-and-infon/a: addresses the other side
lifecycle-client-sends-initialized-after-initMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-client-sends-initialized-after-init
lifecycle-client-no-requests-before-init-responseSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-client-no-requests-before-init-response
lifecycle-server-no-requests-before-initializedSHOULD NOTservertested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-server-no-requests-before-initializedn/a: addresses the other side
lifecycle-capabilities-establish-session-features(definitional)bothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-capabilities-establish-session-featurestested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-capabilities-establish-session-features
lifecycle-respect-negotiated-version-and-capabilities-shouldSHOULDbothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-shouldtested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-should
lifecycle-stdio-client-shutdown-sequenceSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-stdio-client-shutdown-sequence
lifecycle-stdio-server-may-exitMAYservertested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-stdio-server-may-exitn/a: addresses the other side
lifecycle-http-shutdown-closing-connection(definitional)bothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-http-shutdown-closing-connectiontested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-http-shutdown-closing-connection
lifecycle-prepared-for-error-casesSHOULDbothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-prepared-for-error-casestested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-prepared-for-error-cases
lifecycle-timeouts-all-requests-2024SHOULDbothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-timeouts-all-requests-2024tested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-timeouts-all-requests-2024
lifecycle-cancel-not-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-cancel-not-initialize
lifecycle-instructions-may-go-in-system-promptMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-instructions-may-go-in-system-prompt
lifecycle-capability-sets-open(definitional)bothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-capability-sets-opentested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-capability-sets-open
lifecycle-implementation-name-version-requiredREQUIRED (schema field)bothtested: conformance.TestLifecycleServer/2024-11-05/lifecycle/lifecycle-implementation-name-version-requiredtested: conformance.TestLifecycleClient/2024-11-05/lifecycle/lifecycle-implementation-name-version-required

logging

requirementlevelsidemcpx as servermcpx as client
logging-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-declare-capabilityn/a: addresses the other side
logging-client-may-setlevelMAYclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/logging/logging-client-may-setlevel
logging-setlevel-at-or-aboveshould (schema)servertested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-setlevel-at-or-aboven/a: addresses the other side
logging-unset-server-choosesMAYservertested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-unset-server-choosesn/a: addresses the other side
logging-errors-setlevelSHOULDservertested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-errors-setleveln/a: addresses the other side
logging-server-hygieneSHOULDservertested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-server-hygienen/a: addresses the other side
logging-no-secrets-piiMUST NOTboth (emitter)tested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-no-secrets-piitested: conformance.TestUtilitiesClient/2024-11-05/logging/logging-no-secrets-pii
logging-impl-validate-controlSHOULDbothtested: conformance.TestUtilitiesServer/2024-11-05/logging/logging-impl-validate-controltested: conformance.TestUtilitiesClient/2024-11-05/logging/logging-impl-validate-control

messages

requirementlevelsidemcpx as servermcpx as client
messages-all-messages-follow-jsonrpc-2MUSTbothtested: conformance.TestJSONRPCFramesEveryRevision/2024-11-05/messages/all-messages-follow-jsonrpc-2-server-http
conformance.TestJSONRPCFramesEveryRevision/2024-11-05/messages/all-messages-follow-jsonrpc-2-server-stdio
conformance.TestMessagesServer/2024-11-05/messages/messages-all-messages-follow-jsonrpc-2
tested: conformance.TestJSONRPCFramesEveryRevision/2024-11-05/messages/all-messages-follow-jsonrpc-2-client
messages-other-components-mayMAYbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-other-components-maytested: conformance.TestMessagesClient/2024-11-05/messages/messages-other-components-may
messages-request-id-string-or-integerMUSTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-request-id-string-or-integertested: conformance.TestMessagesClient/2024-11-05/messages/messages-request-id-string-or-integer
messages-request-id-not-nullMUST NOTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-request-id-not-nulltested: conformance.TestMessagesClient/2024-11-05/messages/messages-request-id-not-null
messages-request-id-unique-per-sessionMUST NOTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-request-id-unique-per-sessiontested: conformance.TestMessagesClient/2024-11-05/messages/messages-request-id-unique-per-session
messages-response-same-idMUSTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-response-same-idtested: conformance.TestMessagesClient/2024-11-05/messages/messages-response-same-id
messages-response-result-xor-errorMUST / MUST NOTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-response-result-xor-errortested: conformance.TestMessagesClient/2024-11-05/messages/messages-response-result-xor-error
messages-notification-no-idMUST NOTbothtested: conformance.TestMessagesServer/2024-11-05/messages/messages-notification-no-idtested: conformance.TestMessagesClient/2024-11-05/messages/messages-notification-no-id

meta

requirementlevelsidemcpx as servermcpx as client
[meta-reserved-for-mcp](https://modelcontextprotocol.io/specification/GH(2025-03-26) Result._meta)(definitional)bothtested: conformance.TestMessagesServer/2024-11-05/meta/meta-reserved-for-mcptested: conformance.TestMessagesClient/2024-11-05/meta/meta-reserved-for-mcp
meta-progress-token-receiver-not-obligated(definitional)bothtested: conformance.TestMessagesServer/2024-11-05/meta/meta-progress-token-receiver-not-obligatedtested: conformance.TestMessagesClient/2024-11-05/meta/meta-progress-token-receiver-not-obligated

pagination

requirementlevelsidemcpx as servermcpx as client
pagination-no-fixed-page-sizeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/pagination/pagination-no-fixed-page-size
pagination-stable-cursorsSHOULDservertested: conformance.TestUtilitiesServer/2024-11-05/pagination/pagination-stable-cursorsn/a: addresses the other side
pagination-invalid-cursor-gracefulSHOULDservertested: conformance.TestUtilitiesServer/2024-11-05/pagination/pagination-invalid-cursor-gracefuln/a: addresses the other side
pagination-missing-nextcursor-endSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/pagination/pagination-missing-nextcursor-end
pagination-support-both-flowsSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/pagination/pagination-support-both-flows
pagination-cursor-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/pagination/pagination-cursor-opaque
pagination-no-persist-across-sessionsMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2024-11-05/pagination/pagination-no-persist-across-sessions
pagination-invalid-cursor-32602SHOULDservertested: conformance.TestUtilitiesServer/2024-11-05/pagination/pagination-invalid-cursor-32602n/a: addresses the other side

ping

requirementlevelsidemcpx as servermcpx as client
ping-receiver-responds-promptly-emptyMUSTbothtested: conformance.TestLifecycleServer/2024-11-05/ping/ping-receiver-responds-promptly-emptytested: conformance.TestLifecycleClient/2024-11-05/ping/ping-receiver-responds-promptly-empty
ping-either-party-may-initiate(definitional)bothtested: conformance.TestLifecycleServer/2024-11-05/ping/ping-either-party-may-initiatetested: conformance.TestLifecycleClient/2024-11-05/ping/ping-either-party-may-initiate
ping-sender-may-consider-staleMAYbothn/a: catalogue marks it n/an/a: does not ping
ping-periodic-pingsSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
ping-frequency-configurableSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-timeouts-appropriateSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-avoid-excessiveSHOULDbothtested: conformance.TestLifecycleServer/2024-11-05/ping/ping-avoid-excessivetested: conformance.TestLifecycleClient/2024-11-05/ping/ping-avoid-excessive
ping-timeout-is-connection-failureSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-multiple-failures-may-resetMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-log-failuresSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-schema-receiver-must-respondmust (lowercase)bothtested: conformance.TestLifecycleServer/2024-11-05/ping/ping-schema-receiver-must-respondtested: conformance.TestLifecycleClient/2024-11-05/ping/ping-schema-receiver-must-respond

progress

requirementlevelsidemcpx as servermcpx as client
progress-token-string-or-intMUSTboth (requester)n/a: as a server mcpx never requests progress; a host’s token is relayed back verbatimtested: conformance.TestUtilitiesClient/2024-11-05/progress/progress-token-string-or-int
progress-token-unique-senderMUSTbothtested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-token-unique-sendertested: conformance.TestUtilitiesClient/2024-11-05/progress/progress-token-unique-sender
progress-receiver-may-sendMAYbothn/a: optional feature mcpx does not implementn/a: never receives requests with tokens
progress-must-increaseMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-must-increasen/a: catalogue marks it n/a
progress-only-active-tokensMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-only-active-tokensn/a: catalogue marks it n/a
progress-receiver-may-skipMAYbothtested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-receiver-may-skiptested: conformance.TestUtilitiesClient/2024-11-05/progress/progress-receiver-may-skip
progress-track-tokensSHOULDbothn/a: catalogue marks it n/atested: conformance.TestUtilitiesClient/2024-11-05/progress/progress-track-tokens
progress-rate-limitSHOULDbothtested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-rate-limittested: conformance.TestUtilitiesClient/2024-11-05/progress/progress-rate-limit
progress-stop-after-completionMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2024-11-05/progress/progress-stop-after-completionn/a: catalogue marks it n/a

prompts

requirementlevelsidemcpx as servermcpx as client
prompts-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-declare-capabilityn/a: addresses the other side
prompts-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-list-changed-notifyn/a: addresses the other side
prompts-image-base64-mimeMUSTservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-image-base64-mimen/a: addresses the other side
prompts-embedded-must-includeMUSTservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-embedded-must-includen/a: addresses the other side
prompts-validate-argsSHOULDservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-validate-argsn/a: addresses the other side
prompts-respect-capability-negotiationSHOULDbothtested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-respect-capability-negotiationtested: conformance.TestFeaturesClient/2024-11-05/prompts/prompts-respect-capability-negotiation
prompts-validate-ioMUSTbothtested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-validate-io; mcpclient.TestValidatePromptResultcovered

prompts / errors

requirementlevelsidemcpx as servermcpx as client
prompts-errors-32602-32603SHOULDservertested: conformance.TestFeaturesServer/2024-11-05/prompts/prompts-errors-32602-32603n/a: addresses the other side

prompts / pagination

requirementlevelsidemcpx as servermcpx as client
prompts-client-paginateSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2024-11-05/prompts/prompts-client-paginate

resources

requirementlevelsidemcpx as servermcpx as client
resources-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-declare-capabilityn/a: addresses the other side
resources-subscribe-cap-meaning-legacy(definition)servertested: e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/stdio-subscribe-delivers-upstream-updates-under-the-mcpx-uri@2024-11-05,2025-03-26,2025-06-18,2025-11-25n/a: addresses the other side
resources-cap-features-independentMAYservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-cap-features-independentn/a: addresses the other side
resources-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-list-changed-notifyn/a: addresses the other side
resources-updated-may-be-subresource(note, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2024-11-05/resources/resources-updated-may-be-subresource
resources-https-only-if-client-fetchableSHOULDservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-https-only-if-client-fetchablen/a: addresses the other side
resources-https-else-other-schemeSHOULDservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-https-else-other-schemen/a: addresses the other side
resources-file-xdg-mime-mayMAYservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-file-xdg-mime-mayn/a: addresses the other side
resources-sec-validate-urisMUSTservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-sec-validate-urisn/a: addresses the other side
resources-sec-access-controlSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-binary-encodedMUSTservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-sec-binary-encodedn/a: addresses the other side
resources-sec-permissionsSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side

resources / errors

requirementlevelsidemcpx as servermcpx as client
resources-not-found-32002SHOULDservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-not-found-32002n/a: addresses the other side
resources-internal-32603SHOULDservertested: conformance.TestFeaturesServer/2024-11-05/resources/resources-internal-32603n/a: addresses the other side

resources / subscriptions

requirementlevelsidemcpx as servermcpx as client
resources-subscribe-rpc(optional)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2024-11-05/resources/resources-subscribe-rpc
resources-unsubscribe-follows-subscribeshould (lowercase, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2024-11-05/resources/resources-unsubscribe-follows-subscribe

roots

requirementlevelsidemcpx as servermcpx as client
roots-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/roots/roots-client-declare-capability-at-initialize
roots-client-listchanged-must-notifyMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/roots/roots-client-listchanged-must-notify
roots-uri-must-be-file-schemeMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-only-expose-permitted-rootsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-uris-path-traversalMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-implement-access-controlsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-accessibilityMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-handle-unavailable-rootsSHOULDservern/a: mcpx does not consume rootsn/a: addresses the other side
roots-server-respect-boundariesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-validate-paths-against-rootsSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-prompt-consent-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-provide-root-management-uiSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-accessibility-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-root-changesSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-check-capability-before-useSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-handle-list-changesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-cache-root-informationSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-return-jsonrpc-errorsSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/roots/roots-client-return-jsonrpc-errors

sampling

requirementlevelsidemcpx as servermcpx as client
sampling-human-in-loop-can-denySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/sampling/sampling-human-in-loop-can-deny
sampling-app-review-uiSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-view-edit-promptsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-review-responsesSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/sampling/sampling-client-declare-capability-at-initialize
sampling-client-may-sample-fewer-tokensMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-model-preferencesMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-modify-system-promptMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-includecontextMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-evaluated-in-orderMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-over-prioritiesSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-name-substringSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-may-map-providerMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-stopreason-custom-valuesMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/sampling/sampling-stopreason-custom-values
sampling-client-errors-user-rejectedSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/sampling/sampling-client-errors-user-rejected
sampling-client-user-approval-controlsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-both-validate-message-contentSHOULDbothtested: conformance.TestSamplingValidation/2024-11-05/sampling/sampling-both-validate-message-contenttested: conformance.TestSamplingValidation/2024-11-05/sampling/sampling-both-validate-message-content
sampling-client-respect-hintsSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2024-11-05/sampling/sampling-client-rate-limit
sampling-both-handle-sensitive-dataMUSTbothtested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
tested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28

subscriptions

requirementlevelsidemcpx as servermcpx as client
subscriptions-list-changed-should-notifySHOULDservertested: conformance.TestStatefulServer/2024-11-05/subscriptions/subscriptions-list-changed-should-notifyn/a: addresses the other side
subscriptions-resources-updated-only-if-subscribedshould (schema)servertested: conformance.TestStatefulServer/2024-11-05/subscriptions/subscriptions-resources-updated-only-if-subscribedn/a: addresses the other side

tools

requirementlevelsidemcpx as servermcpx as client
tools-human-in-loop-denySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-ui-expose-indicate-confirmSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-declare-capabilityn/a: addresses the other side
tools-list-changed-means-emit(definition)servertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-list-changed-means-emitn/a: addresses the other side
tools-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-list-changed-notifyn/a: addresses the other side
tools-result-iserror-for-tool-errorsSHOULDservertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-result-iserror-for-tool-errorsn/a: addresses the other side
tools-result-lookup-errors-protocolshould (lowercase)servertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-result-lookup-errors-protocoln/a: addresses the other side
tools-error-unknown-tool-protocol(classification)servertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-error-unknown-tool-protocoln/a: addresses the other side
tools-error-invalid-args-protocol(classification)servertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-error-invalid-args-protocoln/a: addresses the other side
tools-embedded-resource-mayMAYservertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-embedded-resource-mayn/a: addresses the other side
tools-server-sec-validate-inputsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-client-secSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement

tools / json-schema

requirementlevelsidemcpx as servermcpx as client
tools-inputschema-type-object-rootREQUIRED (schema)servertested: conformance.TestFeaturesServer/2024-11-05/tools/tools-inputschema-type-object-rootn/a: addresses the other side

transport-http-sse

requirementlevelsidemcpx as servermcpx as client
http-sse-server-validate-originMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-server-bind-localhostSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-server-authenticateSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-server-two-endpointsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-server-endpoint-eventMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-client-post-to-endpointMUSTclientn/a: addresses the other sidetested: mcpclient.TestHTTPSSETransport/2024-11-05/transport-http-sse/client-posts-to-endpoint-and-reads-stream
http-sse-server-messages-as-sse-eventsMUST (implied)servern/a: optional feature mcpx does not implementn/a: addresses the other side

transport-stdio

requirementlevelsidemcpx as servermcpx as client
transport-client-should-support-stdioSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2024-11-05/transport-stdio/transport-client-should-support-stdio
transport-custom-preserve-jsonrpc-and-lifecycleMUSTbothn/a: mcpx serves MCP only over stdio and Streamable HTTP; its /v1 REST surface is not an MCP transportn/a: catalogue marks it n/a
transport-custom-document-patternsSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
stdio-no-embedded-newlinesMUST NOTbothtested: conformance.TestTransportServer/2024-11-05/transport-stdio/stdio-no-embedded-newlinestested: conformance.TestTransportClient/2024-11-05/transport-stdio/stdio-no-embedded-newlines
stdio-server-may-log-to-stderrMAYservertested: conformance.TestTransportServer/2024-11-05/transport-stdio/stdio-server-may-log-to-stderrn/a: addresses the other side
stdio-client-may-capture-stderrMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2024-11-05/transport-stdio/stdio-client-may-capture-stderr
stdio-server-stdout-only-mcpMUST NOTservertested: conformance.TestTransportServer/2024-11-05/transport-stdio/stdio-server-stdout-only-mcpn/a: addresses the other side
stdio-client-stdin-only-mcpMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2024-11-05/transport-stdio/stdio-client-stdin-only-mcp

versioning

requirementlevelsidemcpx as servermcpx as client
lifecycle-client-sends-supported-versionMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/versioning/lifecycle-client-sends-supported-version
lifecycle-client-sends-latest-versionSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/versioning/lifecycle-client-sends-latest-version
lifecycle-client-may-support-olderMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/versioning/lifecycle-client-may-support-older
lifecycle-server-echoes-supported-versionMUSTservertested: conformance.TestLifecycleServer/2024-11-05/versioning/lifecycle-server-echoes-supported-versionn/a: addresses the other side
lifecycle-server-counteroffers-versionMUSTservertested: conformance.TestLifecycleServer/2024-11-05/versioning/lifecycle-server-counteroffers-versionn/a: addresses the other side
lifecycle-server-counteroffers-latestSHOULDservertested: conformance.TestLifecycleServer/2024-11-05/versioning/lifecycle-server-counteroffers-latestn/a: addresses the other side
lifecycle-client-disconnects-on-unsupported-versionSHOULD (prose) / MUST (schema)clientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2024-11-05/versioning/lifecycle-client-disconnects-on-unsupported-version

2025-03-26

(security; not in area list)

requirementlevelsidemcpx as servermcpx as client
security-implementors-guidelinesSHOULDbothtested: conformance.TestMessagesServer/2025-03-26/security/security-implementors-guidelinesn/a: catalogue marks it n/a

authorization

requirementlevelsidemcpx as servermcpx as client
authorization-http-should-conform-stdio-should-notSHOULD / SHOULD NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authorization-custom-auth-mayMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
streamable-http-auth-discovery-version-headerSHOULDclientn/a: addresses the other siden/a: no OAuth
authz-optionalOPTIONALbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-http-should-conformSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-stdio-use-environmentSHOULD NOTbothgap not implemented: #1975gap not implemented: #1950
authz-alt-transport-best-practiceMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-implement-oauth21MUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-dcr-shouldSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-rfc8414-server-should-client-mustSHOULD / MUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-grant-typesSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-401-when-unauthorizedMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-base-url-discards-pathMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-default-endpoints-fallbackMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-bearer-headerMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-token-every-requestMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-no-token-in-queryMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-server-validates-tokenMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-error-status-codesMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-2025-03-26-security-listMUST / SHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-2025-03-26-implementation-reqsMUST / REQUIRED / SHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-third-party-session-bindingMAY / MUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side

cancellation

requirementlevelsidemcpx as servermcpx as client
cancellation-ref-same-direction-in-progressMUSTbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-ref-same-direction-in-progresstested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-ref-same-direction-in-progress
cancellation-client-must-not-cancel-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-client-must-not-cancel-initialize
cancellation-receiver-should-stop-free-no-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-receiver-should-stop-free-no-responsetested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-receiver-should-stop-free-no-response
cancellation-receiver-may-ignoreMAYbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-receiver-may-ignoretested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-receiver-may-ignore
cancellation-sender-ignore-late-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-sender-ignore-late-responsetested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-sender-ignore-late-response
cancellation-handle-racesMUSTbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-handle-racestested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-handle-races
cancellation-log-reasonsSHOULDbothgap #77: a cancellation reason reaches Server.OnCancel, which nothing outside tests sets; it is never loggedtested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-log-reasons
cancellation-ui-indicateSHOULDbothn/a: no UIn/a: no UI
cancellation-ignore-invalidSHOULDbothtested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-ignore-invalidtested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-ignore-invalid
cancellation-processing-should-ceaseSHOULDboth (receiver)tested: conformance.TestUtilitiesServer/2025-03-26/cancellation/cancellation-processing-should-ceasetested: conformance.TestUtilitiesClient/2025-03-26/cancellation/cancellation-processing-should-cease

completion

requirementlevelsidemcpx as servermcpx as client
completion-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-03-26/completion/completion-declare-capabilityn/a: addresses the other side
completion-max-100descr. + schemaservertested: conformance.TestUtilitiesServer/2025-03-26/completion/completion-max-100n/a: addresses the other side
completion-server-shouldSHOULDservertested: daemon.TestHandleCompleteRateLimitHotChangeAndZeroDisables@all
daemon.TestHandleCompleteRateLimitRefusesBeforePool@all
daemon.TestHandleCompleteServerWindowBindsAcrossCallers@all
daemon.TestLocalCompletionCapAfterRanking@all
daemon.TestLocalCompletionTiersPrompts@all
daemon.TestLocalCompletionTiersResources@all
n/a: addresses the other side
completion-client-shouldSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/completion/completion-client-should
completion-securityMUSTbothgap #1968: partial: completion inputs are validated and completion/complete is rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) since #2387, but there is no per-caller access control for sensitive suggestions and no completion-specific information-disclosure control; waiting on principals #1968 / permissive-auth #1972n/a: catalogue marks it n/a
completion-errorsSHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/completion/completion-errorsn/a: addresses the other side

errors

requirementlevelsidemcpx as servermcpx as client
errors-error-has-code-and-messageMUSTbothtested: conformance.TestMessagesServer/2025-03-26/errors/errors-error-has-code-and-messagetested: conformance.TestMessagesClient/2025-03-26/errors/errors-error-has-code-and-message
errors-codes-integersMUSTbothtested: conformance.TestMessagesServer/2025-03-26/errors/errors-codes-integerstested: conformance.TestMessagesClient/2025-03-26/errors/errors-codes-integers
errors-message-concise-single-sentenceSHOULDbothtested: conformance.TestMessagesServer/2025-03-26/errors/errors-message-concise-single-sentencen/a: sends no errors except to server requests

lifecycle

requirementlevelsidemcpx as servermcpx as client
lifecycle-implementations-support-base-and-lifecycleMUSTbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-implementations-support-base-and-lifecycletested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-implementations-support-base-and-lifecycle
lifecycle-initialize-not-in-batchMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-initialize-not-in-batch
lifecycle-initialization-first-interactionMUSTbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-initialization-first-interactiontested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-initialization-first-interaction
lifecycle-client-sends-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-client-sends-initialize
lifecycle-server-responds-caps-and-infoMUSTservertested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-server-responds-caps-and-infon/a: addresses the other side
lifecycle-client-sends-initialized-after-initMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-client-sends-initialized-after-init
lifecycle-client-no-requests-before-init-responseSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-client-no-requests-before-init-response
lifecycle-server-no-requests-before-initializedSHOULD NOTservertested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-server-no-requests-before-initializedn/a: addresses the other side
lifecycle-capabilities-establish-session-features(definitional)bothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-capabilities-establish-session-featurestested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-capabilities-establish-session-features
lifecycle-respect-negotiated-version-and-capabilities-shouldSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-shouldtested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-should
lifecycle-stdio-client-shutdown-sequenceSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-stdio-client-shutdown-sequence
lifecycle-stdio-server-may-exitMAYservertested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-stdio-server-may-exitn/a: addresses the other side
lifecycle-http-shutdown-closing-connection(definitional)bothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-http-shutdown-closing-connectiontested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-http-shutdown-closing-connection
lifecycle-prepared-for-error-casesSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-prepared-for-error-casestested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-prepared-for-error-cases
lifecycle-timeouts-establishSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-timeouts-establishtested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-timeouts-establish
lifecycle-timeouts-cancel-on-expirySHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-timeouts-cancel-on-expirytested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-timeouts-cancel-on-expiry
lifecycle-timeouts-per-request-configurableSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-timeouts-per-request-configurabletested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-timeouts-per-request-configurable
lifecycle-timeouts-may-reset-on-progressMAYbothn/a: catalogue marks it n/atested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-timeouts-may-reset-on-progress
lifecycle-timeouts-max-regardless-of-progressSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-timeouts-max-regardless-of-progresstested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-timeouts-max-regardless-of-progress
lifecycle-cancel-not-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-cancel-not-initialize
lifecycle-instructions-may-go-in-system-promptMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-instructions-may-go-in-system-prompt
lifecycle-capability-sets-open(definitional)bothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-capability-sets-opentested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-capability-sets-open
lifecycle-implementation-name-version-requiredREQUIRED (schema field)bothtested: conformance.TestLifecycleServer/2025-03-26/lifecycle/lifecycle-implementation-name-version-requiredtested: conformance.TestLifecycleClient/2025-03-26/lifecycle/lifecycle-implementation-name-version-required

logging

requirementlevelsidemcpx as servermcpx as client
logging-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-declare-capabilityn/a: addresses the other side
logging-client-may-setlevelMAYclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/logging/logging-client-may-setlevel
logging-setlevel-at-or-aboveshould (schema)servertested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-setlevel-at-or-aboven/a: addresses the other side
logging-unset-server-choosesMAYservertested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-unset-server-choosesn/a: addresses the other side
logging-errors-setlevelSHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-errors-setleveln/a: addresses the other side
logging-server-hygieneSHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-server-hygienen/a: addresses the other side
logging-no-secrets-piiMUST NOTboth (emitter)tested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-no-secrets-piitested: conformance.TestUtilitiesClient/2025-03-26/logging/logging-no-secrets-pii
logging-impl-validate-controlSHOULDbothtested: conformance.TestUtilitiesServer/2025-03-26/logging/logging-impl-validate-controltested: conformance.TestUtilitiesClient/2025-03-26/logging/logging-impl-validate-control

messages

requirementlevelsidemcpx as servermcpx as client
messages-all-messages-follow-jsonrpc-2MUSTbothtested: conformance.TestJSONRPCFramesEveryRevision/2025-03-26/messages/all-messages-follow-jsonrpc-2-server-http
conformance.TestJSONRPCFramesEveryRevision/2025-03-26/messages/all-messages-follow-jsonrpc-2-server-stdio
conformance.TestMessagesServer/2025-03-26/messages/messages-all-messages-follow-jsonrpc-2
tested: conformance.TestJSONRPCFramesEveryRevision/2025-03-26/messages/all-messages-follow-jsonrpc-2-client
messages-other-components-mayMAYbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-other-components-maytested: conformance.TestMessagesClient/2025-03-26/messages/messages-other-components-may
messages-request-id-string-or-integerMUSTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-request-id-string-or-integertested: conformance.TestMessagesClient/2025-03-26/messages/messages-request-id-string-or-integer
messages-request-id-not-nullMUST NOTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-request-id-not-nulltested: conformance.TestMessagesClient/2025-03-26/messages/messages-request-id-not-null
messages-request-id-unique-per-sessionMUST NOTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-request-id-unique-per-sessiontested: conformance.TestMessagesClient/2025-03-26/messages/messages-request-id-unique-per-session
messages-response-same-idMUSTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-response-same-idtested: conformance.TestMessagesClient/2025-03-26/messages/messages-response-same-id
messages-response-result-xor-errorMUST / MUST NOTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-response-result-xor-errortested: conformance.TestMessagesClient/2025-03-26/messages/messages-response-result-xor-error
messages-result-any-objectMAYbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-result-any-objecttested: conformance.TestMessagesClient/2025-03-26/messages/messages-result-any-object
messages-notification-no-responseMUST NOTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-notification-no-responsetested: conformance.TestMessagesClient/2025-03-26/messages/messages-notification-no-response
messages-notification-no-idMUST NOTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-notification-no-idtested: conformance.TestMessagesClient/2025-03-26/messages/messages-notification-no-id
messages-batch-may-send-must-receiveMAY / MUSTbothtested: conformance.TestMessagesServer/2025-03-26/messages/messages-batch-may-send-must-receivetested: conformance.TestMessagesClient/2025-03-26/messages/messages-batch-may-send-must-receive
transport-jsonrpc-utf8MUSTbothtested: conformance.TestMessagesServer/2025-03-26/messages/transport-jsonrpc-utf8tested: conformance.TestMessagesClient/2025-03-26/messages/transport-jsonrpc-utf8

meta

requirementlevelsidemcpx as servermcpx as client
[meta-reserved-for-mcp](https://modelcontextprotocol.io/specification/GH(2025-03-26) Result._meta)(definitional)bothtested: conformance.TestMessagesServer/2025-03-26/meta/meta-reserved-for-mcptested: conformance.TestMessagesClient/2025-03-26/meta/meta-reserved-for-mcp
meta-progress-token-receiver-not-obligated(definitional)bothtested: conformance.TestMessagesServer/2025-03-26/meta/meta-progress-token-receiver-not-obligatedtested: conformance.TestMessagesClient/2025-03-26/meta/meta-progress-token-receiver-not-obligated

pagination

requirementlevelsidemcpx as servermcpx as client
pagination-no-fixed-page-sizeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/pagination/pagination-no-fixed-page-size
pagination-stable-cursorsSHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/pagination/pagination-stable-cursorsn/a: addresses the other side
pagination-invalid-cursor-gracefulSHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/pagination/pagination-invalid-cursor-gracefuln/a: addresses the other side
pagination-missing-nextcursor-endSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/pagination/pagination-missing-nextcursor-end
pagination-support-both-flowsSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/pagination/pagination-support-both-flows
pagination-cursor-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/pagination/pagination-cursor-opaque
pagination-no-persist-across-sessionsMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-03-26/pagination/pagination-no-persist-across-sessions
pagination-invalid-cursor-32602SHOULDservertested: conformance.TestUtilitiesServer/2025-03-26/pagination/pagination-invalid-cursor-32602n/a: addresses the other side

ping

requirementlevelsidemcpx as servermcpx as client
ping-receiver-responds-promptly-emptyMUSTbothtested: conformance.TestLifecycleServer/2025-03-26/ping/ping-receiver-responds-promptly-emptytested: conformance.TestLifecycleClient/2025-03-26/ping/ping-receiver-responds-promptly-empty
ping-either-party-may-initiate(definitional)bothtested: conformance.TestLifecycleServer/2025-03-26/ping/ping-either-party-may-initiatetested: conformance.TestLifecycleClient/2025-03-26/ping/ping-either-party-may-initiate
ping-sender-may-consider-staleMAYbothn/a: catalogue marks it n/an/a: does not ping
ping-periodic-pingsSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
ping-frequency-configurableSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-timeouts-appropriateSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-avoid-excessiveSHOULDbothtested: conformance.TestLifecycleServer/2025-03-26/ping/ping-avoid-excessivetested: conformance.TestLifecycleClient/2025-03-26/ping/ping-avoid-excessive
ping-timeout-is-connection-failureSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-multiple-failures-may-resetMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-log-failuresSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-schema-receiver-must-respondmust (lowercase)bothtested: conformance.TestLifecycleServer/2025-03-26/ping/ping-schema-receiver-must-respondtested: conformance.TestLifecycleClient/2025-03-26/ping/ping-schema-receiver-must-respond

progress

requirementlevelsidemcpx as servermcpx as client
progress-token-string-or-intMUSTboth (requester)n/a: as a server mcpx never requests progress; a host’s token is relayed back verbatimtested: conformance.TestUtilitiesClient/2025-03-26/progress/progress-token-string-or-int
progress-token-unique-senderMUSTbothtested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-token-unique-sendertested: conformance.TestUtilitiesClient/2025-03-26/progress/progress-token-unique-sender
progress-receiver-may-sendMAYbothn/a: optional feature mcpx does not implementn/a: never receives requests with tokens
progress-must-increaseMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-must-increasen/a: catalogue marks it n/a
progress-message-human-readableSHOULDboth (notifier)tested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-message-human-readablen/a: catalogue marks it n/a
progress-only-active-tokensMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-only-active-tokensn/a: catalogue marks it n/a
progress-receiver-may-skipMAYbothtested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-receiver-may-skiptested: conformance.TestUtilitiesClient/2025-03-26/progress/progress-receiver-may-skip
progress-track-tokensSHOULDbothn/a: catalogue marks it n/atested: conformance.TestUtilitiesClient/2025-03-26/progress/progress-track-tokens
progress-rate-limitSHOULDbothtested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-rate-limittested: conformance.TestUtilitiesClient/2025-03-26/progress/progress-rate-limit
progress-stop-after-completionMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-03-26/progress/progress-stop-after-completionn/a: catalogue marks it n/a

prompts

requirementlevelsidemcpx as servermcpx as client
prompts-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-declare-capabilityn/a: addresses the other side
prompts-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-list-changed-notifyn/a: addresses the other side
prompts-image-base64-mimeMUSTservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-image-base64-mimen/a: addresses the other side
prompts-audio-base64-mimeMUST (not bolded)servertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-audio-base64-mimen/a: addresses the other side
prompts-embedded-must-includeMUSTservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-embedded-must-includen/a: addresses the other side
prompts-validate-argsSHOULDservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-validate-argsn/a: addresses the other side
prompts-respect-capability-negotiationSHOULDbothtested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-respect-capability-negotiationtested: conformance.TestFeaturesClient/2025-03-26/prompts/prompts-respect-capability-negotiation
prompts-validate-ioMUSTbothtested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-validate-io; mcpclient.TestValidatePromptResultcovered

prompts / errors

requirementlevelsidemcpx as servermcpx as client
prompts-errors-32602-32603SHOULDservertested: conformance.TestFeaturesServer/2025-03-26/prompts/prompts-errors-32602-32603n/a: addresses the other side

prompts / pagination

requirementlevelsidemcpx as servermcpx as client
prompts-client-paginateSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-03-26/prompts/prompts-client-paginate

resources

requirementlevelsidemcpx as servermcpx as client
resources-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-declare-capabilityn/a: addresses the other side
resources-subscribe-cap-meaning-legacy(definition)servertested: e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/http-subscribe-delivers-upstream-updates-on-the-get-stream@2025-03-26,2025-06-18,2025-11-25
e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/stdio-subscribe-delivers-upstream-updates-under-the-mcpx-uri@2024-11-05,2025-03-26,2025-06-18,2025-11-25
n/a: addresses the other side
resources-cap-features-independentMAYservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-cap-features-independentn/a: addresses the other side
resources-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-list-changed-notifyn/a: addresses the other side
resources-updated-may-be-subresource(note, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-03-26/resources/resources-updated-may-be-subresource
resources-https-only-if-client-fetchableSHOULDservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-https-only-if-client-fetchablen/a: addresses the other side
resources-https-else-other-schemeSHOULDservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-https-else-other-schemen/a: addresses the other side
resources-file-xdg-mime-mayMAYservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-file-xdg-mime-mayn/a: addresses the other side
resources-sec-validate-urisMUSTservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-sec-validate-urisn/a: addresses the other side
resources-sec-access-controlSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-binary-encodedMUSTservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-sec-binary-encodedn/a: addresses the other side
resources-sec-permissionsSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side

resources / errors

requirementlevelsidemcpx as servermcpx as client
resources-not-found-32002SHOULDservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-not-found-32002n/a: addresses the other side
resources-internal-32603SHOULDservertested: conformance.TestFeaturesServer/2025-03-26/resources/resources-internal-32603n/a: addresses the other side

resources / subscriptions

requirementlevelsidemcpx as servermcpx as client
resources-subscribe-rpc(optional)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-03-26/resources/resources-subscribe-rpc
resources-unsubscribe-follows-subscribeshould (lowercase, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-03-26/resources/resources-unsubscribe-follows-subscribe

roots

requirementlevelsidemcpx as servermcpx as client
roots-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/roots/roots-client-declare-capability-at-initialize
roots-client-listchanged-must-notifyMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/roots/roots-client-listchanged-must-notify
roots-uri-must-be-file-schemeMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-only-expose-permitted-rootsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-uris-path-traversalMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-implement-access-controlsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-accessibilityMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-handle-unavailable-rootsSHOULDservern/a: mcpx does not consume rootsn/a: addresses the other side
roots-server-respect-boundariesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-validate-paths-against-rootsSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-prompt-consent-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-provide-root-management-uiSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-accessibility-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-root-changesSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-check-capability-before-useSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-handle-list-changesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-cache-root-informationSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-return-jsonrpc-errorsSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/roots/roots-client-return-jsonrpc-errors

sampling

requirementlevelsidemcpx as servermcpx as client
sampling-human-in-loop-can-denySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/sampling/sampling-human-in-loop-can-deny
sampling-app-review-uiSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-view-edit-promptsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-review-responsesSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/sampling/sampling-client-declare-capability-at-initialize
sampling-client-may-sample-fewer-tokensMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-model-preferencesMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-modify-system-promptMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-includecontextMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-evaluated-in-orderMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-over-prioritiesSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-name-substringSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-may-map-providerMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-stopreason-custom-valuesMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/sampling/sampling-stopreason-custom-values
sampling-client-errors-user-rejectedSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/sampling/sampling-client-errors-user-rejected
sampling-client-user-approval-controlsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-both-validate-message-contentSHOULDbothtested: conformance.TestSamplingValidation/2025-03-26/sampling/sampling-both-validate-message-contenttested: conformance.TestSamplingValidation/2025-03-26/sampling/sampling-both-validate-message-content
sampling-client-respect-hintsSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-03-26/sampling/sampling-client-rate-limit
sampling-both-handle-sensitive-dataMUSTbothtested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
tested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28

subscriptions

requirementlevelsidemcpx as servermcpx as client
subscriptions-list-changed-should-notifySHOULDservertested: conformance.TestStatefulServer/2025-03-26/subscriptions/subscriptions-list-changed-should-notifyn/a: addresses the other side
subscriptions-resources-updated-only-if-subscribedshould (schema)servertested: conformance.TestStatefulServer/2025-03-26/subscriptions/subscriptions-resources-updated-only-if-subscribedn/a: addresses the other side

tools

requirementlevelsidemcpx as servermcpx as client
tools-human-in-loop-denySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-ui-expose-indicate-confirmSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-declare-capabilityn/a: addresses the other side
tools-list-changed-means-emit(definition)servertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-list-changed-means-emitn/a: addresses the other side
tools-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-list-changed-notifyn/a: addresses the other side
tools-annotations-untrustedMUSTclientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-annotations-hints-no-decisionsshould (lowercase, schema)clientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-result-iserror-for-tool-errorsSHOULDservertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-result-iserror-for-tool-errorsn/a: addresses the other side
tools-result-lookup-errors-protocolshould (lowercase)servertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-result-lookup-errors-protocoln/a: addresses the other side
tools-error-unknown-tool-protocol(classification)servertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-error-unknown-tool-protocoln/a: addresses the other side
tools-error-invalid-args-protocol(classification)servertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-error-invalid-args-protocoln/a: addresses the other side
tools-embedded-resource-mayMAYservertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-embedded-resource-mayn/a: addresses the other side
tools-server-sec-validate-inputsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-client-secSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement

tools / json-schema

requirementlevelsidemcpx as servermcpx as client
tools-inputschema-type-object-rootREQUIRED (schema)servertested: conformance.TestFeaturesServer/2025-03-26/tools/tools-inputschema-type-object-rootn/a: addresses the other side

transport-http-sse

requirementlevelsidemcpx as servermcpx as client
http-sse-server-backcompat-host-bothshould (lowercase)servern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-client-backcompat-probe-any-4xxshould (lowercase)clientn/a: addresses the other sidetested: pool.TestHTTPSSEFallback/2025-11-25/transport-http-sse/client-falls-back-to-get-endpoint-and-caches-it@2025-03-26,2025-06-18

transport-stdio

requirementlevelsidemcpx as servermcpx as client
transport-client-should-support-stdioSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-stdio/transport-client-should-support-stdio
transport-custom-preserve-jsonrpc-and-lifecycleMUSTbothn/a: mcpx serves MCP only over stdio and Streamable HTTP; its /v1 REST surface is not an MCP transportn/a: catalogue marks it n/a
transport-custom-document-patternsSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
stdio-no-embedded-newlinesMUST NOTbothtested: conformance.TestTransportServer/2025-03-26/transport-stdio/stdio-no-embedded-newlinestested: conformance.TestTransportClient/2025-03-26/transport-stdio/stdio-no-embedded-newlines
stdio-message-may-be-batchMAY (implied)bothtested: conformance.TestTransportServer/2025-03-26/transport-stdio/stdio-message-may-be-batchtested: mcpclient.TestReceivedBatchesAreSplit/2025-03-26/messages/client-accepts-batched-server-messages
stdio-server-may-log-to-stderrMAYservertested: conformance.TestTransportServer/2025-03-26/transport-stdio/stdio-server-may-log-to-stderrn/a: addresses the other side
stdio-client-may-capture-stderrMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-stdio/stdio-client-may-capture-stderr
stdio-server-stdout-only-mcpMUST NOTservertested: conformance.TestTransportServer/2025-03-26/transport-stdio/stdio-server-stdout-only-mcpn/a: addresses the other side
stdio-client-stdin-only-mcpMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-stdio/stdio-client-stdin-only-mcp

transport-streamable-http

requirementlevelsidemcpx as servermcpx as client
streamable-http-server-single-endpoint-post-getMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-single-endpoint-post-getn/a: addresses the other side
streamable-http-server-validate-originMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-validate-originn/a: addresses the other side
streamable-http-server-bind-localhostSHOULDservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-bind-localhostn/a: addresses the other side
streamable-http-server-authenticateSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-post-every-messageMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-post-every-message
streamable-http-client-accept-bothMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-accept-both
streamable-http-post-body-may-batchMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-post-body-may-batch
streamable-http-server-202-for-non-requestMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-202-for-non-requestn/a: addresses the other side
streamable-http-server-error-status-if-rejectedMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-error-status-if-rejectedn/a: addresses the other side
streamable-http-server-json-or-sse-for-requestMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-json-or-sse-for-requestn/a: addresses the other side
streamable-http-client-supports-json-and-sseMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-supports-json-and-sse
streamable-http-sse-includes-responseSHOULDservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-sse-includes-responsen/a: addresses the other side
streamable-http-post-sse-may-carry-requestsMAYservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-post-sse-may-carry-requestsn/a: addresses the other side
streamable-http-sse-not-closed-before-responseSHOULD NOTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-sse-not-closed-before-responsen/a: addresses the other side
streamable-http-server-terminates-sse-after-responseSHOULDservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-terminates-sse-after-responsen/a: addresses the other side
streamable-http-disconnect-not-cancelSHOULD NOTbothtested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-disconnect-not-canceln/a: catalogue marks it n/a
streamable-http-client-cancel-by-notificationSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-cancel-by-notification
streamable-http-server-may-resumeMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-may-getMAYclientn/a: addresses the other siden/a: optional feature mcpx does not implement
streamable-http-get-accept-sseMUSTclientn/a: addresses the other siden/a: no GET
streamable-http-get-sse-or-405MUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-get-sse-or-405n/a: addresses the other side
streamable-http-get-stream-unrelatedSHOULDservern/a: 405n/a: addresses the other side
streamable-http-get-no-responsesMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-get-close-any-timeMAYbothtested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-get-close-any-timetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-get-close-any-time
streamable-http-multiple-streamsMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-multiple-streams
streamable-http-no-broadcastMUST / MUST NOTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-no-broadcastn/a: addresses the other side
streamable-http-event-id-uniqueMUSTservern/a: no idsn/a: addresses the other side
streamable-http-client-resume-with-last-event-idSHOULDclientn/a: addresses the other sidetested: mcpclient.TestLegacyHTTPStreamResumption/2025-11-25/transport/client-resumes-closed-stream-with-last-event-id-after-retry@2025-03-26,2025-06-18,2025-11-25
streamable-http-server-no-cross-stream-replayMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-server-may-assign-sessionMAYservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-may-assign-sessionn/a: addresses the other side
streamable-http-session-id-secureSHOULDservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-session-id-securen/a: addresses the other side
streamable-http-session-id-visible-asciiMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-session-id-visible-asciin/a: addresses the other side
streamable-http-client-echoes-sessionMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-echoes-session
streamable-http-server-400-missing-sessionSHOULDservern/a: mcpx does not require one (stateless fallbackn/a: addresses the other side
streamable-http-server-404-terminated-sessionMUSTservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-404-terminated-sessionn/a: addresses the other side
streamable-http-client-reinit-on-404MUSTclientn/a: addresses the other sidetested: mcpclient.TestLegacySession404Recovery/2025-11-25/transport/client-reinitializes-on-session-404-and-retries-once@2025-03-26,2025-06-18,2025-11-25
streamable-http-client-delete-sessionSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-03-26/transport-streamable-http/streamable-http-client-delete-session
streamable-http-server-may-405-deleteMAYservertested: conformance.TestTransportServer/2025-03-26/transport-streamable-http/streamable-http-server-may-405-deleten/a: addresses the other side

versioning

requirementlevelsidemcpx as servermcpx as client
lifecycle-client-sends-supported-versionMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/versioning/lifecycle-client-sends-supported-version
lifecycle-client-sends-latest-versionSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/versioning/lifecycle-client-sends-latest-version
lifecycle-client-may-support-olderMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/versioning/lifecycle-client-may-support-older
lifecycle-server-echoes-supported-versionMUSTservertested: conformance.TestLifecycleServer/2025-03-26/versioning/lifecycle-server-echoes-supported-versionn/a: addresses the other side
lifecycle-server-counteroffers-versionMUSTservertested: conformance.TestLifecycleServer/2025-03-26/versioning/lifecycle-server-counteroffers-versionn/a: addresses the other side
lifecycle-server-counteroffers-latestSHOULDservertested: conformance.TestLifecycleServer/2025-03-26/versioning/lifecycle-server-counteroffers-latestn/a: addresses the other side
lifecycle-client-disconnects-on-unsupported-versionSHOULD (prose) / MUST (schema)clientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-03-26/versioning/lifecycle-client-disconnects-on-unsupported-version

2025-06-18

(security; not in area list)

requirementlevelsidemcpx as servermcpx as client
security-implementors-guidelinesSHOULDbothtested: conformance.TestMessagesServer/2025-06-18/security/security-implementors-guidelinesn/a: catalogue marks it n/a

authorization

requirementlevelsidemcpx as servermcpx as client
authorization-http-should-conform-stdio-should-notSHOULD / SHOULD NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authorization-custom-auth-mayMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-optionalOPTIONALbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-http-should-conformSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-stdio-use-environmentSHOULD NOTbothgap not implemented: #1975gap not implemented: #1950
authz-alt-transport-best-practiceMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-as-implements-oauth21MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-dcr-shouldSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-bearer-headerMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-token-every-requestMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-no-token-in-queryMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-server-validates-tokenMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-validates-audienceMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-client-sends-only-own-as-tokensMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-accept-only-own-tokensMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-no-token-passthroughMUST NOTservergap not implemented: #1975n/a: addresses the other side
authz-error-status-codesMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-prm-rfc9728MUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-prm-lists-authorization-serversMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-as-metadata-rfc8414MUSTbothn/a: catalogue marks it n/an/a: optional feature mcpx does not implement
authz-401-www-authenticateMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-client-parses-www-authenticateMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-client-rfc8414-discoveryMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-resource-indicatorMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-canonical-uri-guidanceSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-secure-token-storageMUSTbothn/a: catalogue marks it n/agap not implemented: #1950
authz-as-short-lived-and-rotateSHOULD / MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-https-and-redirect-urisMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-pkceMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-open-redirectionMUST / SHOULDclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-proxy-consent-per-clientMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-follow-security-considerationsMUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement

cancellation

requirementlevelsidemcpx as servermcpx as client
cancellation-ref-same-direction-in-progressMUSTbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-ref-same-direction-in-progresstested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-ref-same-direction-in-progress
cancellation-client-must-not-cancel-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-client-must-not-cancel-initialize
cancellation-receiver-should-stop-free-no-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-receiver-should-stop-free-no-responsetested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-receiver-should-stop-free-no-response
cancellation-receiver-may-ignoreMAYbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-receiver-may-ignoretested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-receiver-may-ignore
cancellation-sender-ignore-late-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-sender-ignore-late-responsetested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-sender-ignore-late-response
cancellation-handle-racesMUSTbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-handle-racestested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-handle-races
cancellation-log-reasonsSHOULDbothgap #77: a cancellation reason reaches Server.OnCancel, which nothing outside tests sets; it is never loggedtested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-log-reasons
cancellation-ui-indicateSHOULDbothn/a: no UIn/a: no UI
cancellation-ignore-invalidSHOULDbothtested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-ignore-invalidtested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-ignore-invalid
cancellation-processing-should-ceaseSHOULDboth (receiver)tested: conformance.TestUtilitiesServer/2025-06-18/cancellation/cancellation-processing-should-ceasetested: conformance.TestUtilitiesClient/2025-06-18/cancellation/cancellation-processing-should-cease

completion

requirementlevelsidemcpx as servermcpx as client
completion-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-06-18/completion/completion-declare-capabilityn/a: addresses the other side
completion-max-100descr. + schemaservertested: conformance.TestUtilitiesServer/2025-06-18/completion/completion-max-100n/a: addresses the other side
completion-server-shouldSHOULDservertested: daemon.TestHandleCompleteRateLimitHotChangeAndZeroDisables@all
daemon.TestHandleCompleteRateLimitRefusesBeforePool@all
daemon.TestHandleCompleteServerWindowBindsAcrossCallers@all
daemon.TestLocalCompletionCapAfterRanking@all
daemon.TestLocalCompletionTiersPrompts@all
daemon.TestLocalCompletionTiersResources@all
n/a: addresses the other side
completion-client-shouldSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/completion/completion-client-should
completion-securityMUSTbothgap #1968: partial: completion inputs are validated and completion/complete is rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) since #2387, but there is no per-caller access control for sensitive suggestions and no completion-specific information-disclosure control; waiting on principals #1968 / permissive-auth #1972n/a: catalogue marks it n/a
completion-errorsSHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/completion/completion-errorsn/a: addresses the other side
completion-context-argumentsshould (lowercase)clientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/completion/completion-context-arguments

elicitation

requirementlevelsidemcpx as servermcpx as client
elicitation-server-no-sensitive-infoMUST NOTservertested: conformance.TestClientFeaturesServer/2025-06-18/elicitation/elicitation-server-no-sensitive-infon/a: addresses the other side
elicitation-app-show-requesting-serverSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/elicitation/elicitation-app-show-requesting-server
elicitation-app-review-modify-responsesSHOULDclientn/a: addresses the other sidetested: cli.TestRenderAnswerReviewMasksSensitiveFields@2025-06-18
cli.TestRenderQuestionFormShowsSchemaDefaults@2025-06-18
elicitation-app-decline-cancel-optionsSHOULDclientn/a: addresses the other sidetested: elicit.TestAnExpiredQuestionBecomesCancelNotDecline@2025-06-18,2025-11-25,2026-07-28
elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/elicitation/elicitation-client-declare-capability-at-initialize
elicitation-form-schema-flat-primitivesMUST (schema-structural)servertested: conformance.TestClientFeaturesServer/2025-06-18/elicitation/elicitation-form-schema-flat-primitivesn/a: addresses the other side
elicitation-client-user-approval-controlsSHOULDclientn/a: addresses the other sidetested: elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28
elicitation-client-allow-decline-any-timeSHOULDclientn/a: addresses the other sidetested: elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/elicitation/elicitation-client-rate-limit
elicitation-client-clear-what-and-whySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/elicitation/elicitation-client-clear-what-and-why
elicitation-both-validate-against-schemaSHOULDbothtested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18tested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18

errors

requirementlevelsidemcpx as servermcpx as client
errors-error-has-code-and-messageMUSTbothtested: conformance.TestMessagesServer/2025-06-18/errors/errors-error-has-code-and-messagetested: conformance.TestMessagesClient/2025-06-18/errors/errors-error-has-code-and-message
errors-codes-integersMUSTbothtested: conformance.TestMessagesServer/2025-06-18/errors/errors-codes-integerstested: conformance.TestMessagesClient/2025-06-18/errors/errors-codes-integers
errors-message-concise-single-sentenceSHOULDbothtested: conformance.TestMessagesServer/2025-06-18/errors/errors-message-concise-single-sentencen/a: sends no errors except to server requests

lifecycle

requirementlevelsidemcpx as servermcpx as client
lifecycle-implementations-support-base-and-lifecycleMUSTbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-implementations-support-base-and-lifecycletested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-implementations-support-base-and-lifecycle
lifecycle-initialization-first-interactionMUSTbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-initialization-first-interactiontested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-initialization-first-interaction
lifecycle-client-sends-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-client-sends-initialize
lifecycle-server-responds-caps-and-infoMUSTservertested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-server-responds-caps-and-infon/a: addresses the other side
lifecycle-client-sends-initialized-after-initMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-client-sends-initialized-after-init
lifecycle-client-no-requests-before-init-responseSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-client-no-requests-before-init-response
lifecycle-server-no-requests-before-initializedSHOULD NOTservertested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-server-no-requests-before-initializedn/a: addresses the other side
lifecycle-capabilities-establish-session-features(definitional)bothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-capabilities-establish-session-featurestested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-capabilities-establish-session-features
lifecycle-respect-negotiated-version-and-capabilities-mustMUSTbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-musttested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-must
lifecycle-stdio-client-shutdown-sequenceSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-stdio-client-shutdown-sequence
lifecycle-stdio-server-may-exitMAYservertested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-stdio-server-may-exitn/a: addresses the other side
lifecycle-http-shutdown-closing-connection(definitional)bothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-http-shutdown-closing-connectiontested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-http-shutdown-closing-connection
lifecycle-prepared-for-error-casesSHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-prepared-for-error-casestested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-prepared-for-error-cases
lifecycle-timeouts-establishSHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-timeouts-establishtested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-timeouts-establish
lifecycle-timeouts-cancel-on-expirySHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-timeouts-cancel-on-expirytested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-timeouts-cancel-on-expiry
lifecycle-timeouts-per-request-configurableSHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-timeouts-per-request-configurabletested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-timeouts-per-request-configurable
lifecycle-timeouts-may-reset-on-progressMAYbothn/a: catalogue marks it n/atested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-timeouts-may-reset-on-progress
lifecycle-timeouts-max-regardless-of-progressSHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-timeouts-max-regardless-of-progresstested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-timeouts-max-regardless-of-progress
lifecycle-cancel-not-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-cancel-not-initialize
lifecycle-instructions-may-go-in-system-promptMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-instructions-may-go-in-system-prompt
lifecycle-capability-sets-open(definitional)bothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-capability-sets-opentested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-capability-sets-open
lifecycle-implementation-name-version-requiredREQUIRED (schema field)bothtested: conformance.TestLifecycleServer/2025-06-18/lifecycle/lifecycle-implementation-name-version-requiredtested: conformance.TestLifecycleClient/2025-06-18/lifecycle/lifecycle-implementation-name-version-required

logging

requirementlevelsidemcpx as servermcpx as client
logging-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-declare-capabilityn/a: addresses the other side
logging-client-may-setlevelMAYclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/logging/logging-client-may-setlevel
logging-setlevel-at-or-aboveshould (schema)servertested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-setlevel-at-or-aboven/a: addresses the other side
logging-unset-server-choosesMAYservertested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-unset-server-choosesn/a: addresses the other side
logging-errors-setlevelSHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-errors-setleveln/a: addresses the other side
logging-server-hygieneSHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-server-hygienen/a: addresses the other side
logging-no-secrets-piiMUST NOTboth (emitter)tested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-no-secrets-piitested: conformance.TestUtilitiesClient/2025-06-18/logging/logging-no-secrets-pii
logging-impl-validate-controlSHOULDbothtested: conformance.TestUtilitiesServer/2025-06-18/logging/logging-impl-validate-controltested: conformance.TestUtilitiesClient/2025-06-18/logging/logging-impl-validate-control

messages

requirementlevelsidemcpx as servermcpx as client
messages-all-messages-follow-jsonrpc-2MUSTbothtested: conformance.TestJSONRPCFramesEveryRevision/2025-06-18/messages/all-messages-follow-jsonrpc-2-server-http
conformance.TestJSONRPCFramesEveryRevision/2025-06-18/messages/all-messages-follow-jsonrpc-2-server-stdio
conformance.TestMessagesServer/2025-06-18/messages/messages-all-messages-follow-jsonrpc-2
tested: conformance.TestJSONRPCFramesEveryRevision/2025-06-18/messages/all-messages-follow-jsonrpc-2-client
messages-other-components-mayMAYbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-other-components-maytested: conformance.TestMessagesClient/2025-06-18/messages/messages-other-components-may
messages-request-id-string-or-integerMUSTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-request-id-string-or-integertested: conformance.TestMessagesClient/2025-06-18/messages/messages-request-id-string-or-integer
messages-request-id-not-nullMUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-request-id-not-nulltested: conformance.TestMessagesClient/2025-06-18/messages/messages-request-id-not-null
messages-request-id-unique-per-sessionMUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-request-id-unique-per-sessiontested: conformance.TestMessagesClient/2025-06-18/messages/messages-request-id-unique-per-session
messages-response-same-idMUSTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-response-same-idtested: conformance.TestMessagesClient/2025-06-18/messages/messages-response-same-id
messages-response-result-xor-errorMUST / MUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-response-result-xor-errortested: conformance.TestMessagesClient/2025-06-18/messages/messages-response-result-xor-error
messages-result-any-objectMAYbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-result-any-objecttested: conformance.TestMessagesClient/2025-06-18/messages/messages-result-any-object
messages-notification-no-responseMUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-notification-no-responsetested: conformance.TestMessagesClient/2025-06-18/messages/messages-notification-no-response
messages-notification-no-idMUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/messages/messages-notification-no-idtested: conformance.TestMessagesClient/2025-06-18/messages/messages-notification-no-id
transport-jsonrpc-utf8MUSTbothtested: conformance.TestMessagesServer/2025-06-18/messages/transport-jsonrpc-utf8tested: conformance.TestMessagesClient/2025-06-18/messages/transport-jsonrpc-utf8

meta

requirementlevelsidemcpx as servermcpx as client
meta-no-assumptions-on-reserved-keysMUST NOTbothtested: conformance.TestMessagesServer/2025-06-18/meta/meta-no-assumptions-on-reserved-keystested: conformance.TestMessagesClient/2025-06-18/meta/meta-no-assumptions-on-reserved-keys
meta-prefix-label-formatMUSTbothtested: conformance.TestMessagesServer/2025-06-18/meta/meta-prefix-label-formattested: conformance.TestMessagesClient/2025-06-18/meta/meta-prefix-label-format
meta-reserved-prefix-2025-06-18(reservation)bothn/a: catalogue marks it n/an/a: catalogue marks it n/a
meta-name-formatMUST / MAYbothtested: conformance.TestMessagesServer/2025-06-18/meta/meta-name-formattested: conformance.TestMessagesClient/2025-06-18/meta/meta-name-format
meta-schema-may-reserve-names(reservation)bothtested: conformance.TestMessagesServer/2025-06-18/meta/meta-schema-may-reserve-namestested: conformance.TestMessagesClient/2025-06-18/meta/meta-schema-may-reserve-names
meta-progress-token-receiver-not-obligated(definitional)bothtested: conformance.TestMessagesServer/2025-06-18/meta/meta-progress-token-receiver-not-obligatedtested: conformance.TestMessagesClient/2025-06-18/meta/meta-progress-token-receiver-not-obligated

pagination

requirementlevelsidemcpx as servermcpx as client
pagination-no-fixed-page-sizeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/pagination/pagination-no-fixed-page-size
pagination-stable-cursorsSHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/pagination/pagination-stable-cursorsn/a: addresses the other side
pagination-invalid-cursor-gracefulSHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/pagination/pagination-invalid-cursor-gracefuln/a: addresses the other side
pagination-missing-nextcursor-endSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/pagination/pagination-missing-nextcursor-end
pagination-support-both-flowsSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/pagination/pagination-support-both-flows
pagination-cursor-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/pagination/pagination-cursor-opaque
pagination-no-persist-across-sessionsMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-06-18/pagination/pagination-no-persist-across-sessions
pagination-invalid-cursor-32602SHOULDservertested: conformance.TestUtilitiesServer/2025-06-18/pagination/pagination-invalid-cursor-32602n/a: addresses the other side

ping

requirementlevelsidemcpx as servermcpx as client
ping-receiver-responds-promptly-emptyMUSTbothtested: conformance.TestLifecycleServer/2025-06-18/ping/ping-receiver-responds-promptly-emptytested: conformance.TestLifecycleClient/2025-06-18/ping/ping-receiver-responds-promptly-empty
ping-either-party-may-initiate(definitional)bothtested: conformance.TestLifecycleServer/2025-06-18/ping/ping-either-party-may-initiatetested: conformance.TestLifecycleClient/2025-06-18/ping/ping-either-party-may-initiate
ping-sender-may-consider-staleMAYbothn/a: catalogue marks it n/an/a: does not ping
ping-periodic-pingsSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
ping-frequency-configurableSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-timeouts-appropriateSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-avoid-excessiveSHOULDbothtested: conformance.TestLifecycleServer/2025-06-18/ping/ping-avoid-excessivetested: conformance.TestLifecycleClient/2025-06-18/ping/ping-avoid-excessive
ping-timeout-is-connection-failureSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-multiple-failures-may-resetMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-log-failuresSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-schema-receiver-must-respondmust (lowercase)bothtested: conformance.TestLifecycleServer/2025-06-18/ping/ping-schema-receiver-must-respondtested: conformance.TestLifecycleClient/2025-06-18/ping/ping-schema-receiver-must-respond

progress

requirementlevelsidemcpx as servermcpx as client
progress-token-string-or-intMUSTboth (requester)n/a: as a server mcpx never requests progress; a host’s token is relayed back verbatimtested: conformance.TestUtilitiesClient/2025-06-18/progress/progress-token-string-or-int
progress-token-unique-senderMUSTbothtested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-token-unique-sendertested: conformance.TestUtilitiesClient/2025-06-18/progress/progress-token-unique-sender
progress-receiver-may-sendMAYbothn/a: optional feature mcpx does not implementn/a: never receives requests with tokens
progress-must-increaseMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-must-increasen/a: catalogue marks it n/a
progress-message-human-readableSHOULDboth (notifier)tested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-message-human-readablen/a: catalogue marks it n/a
progress-only-active-tokensMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-only-active-tokensn/a: catalogue marks it n/a
progress-receiver-may-skipMAYbothtested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-receiver-may-skiptested: conformance.TestUtilitiesClient/2025-06-18/progress/progress-receiver-may-skip
progress-track-tokensSHOULDbothn/a: catalogue marks it n/atested: conformance.TestUtilitiesClient/2025-06-18/progress/progress-track-tokens
progress-rate-limitSHOULDbothtested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-rate-limittested: conformance.TestUtilitiesClient/2025-06-18/progress/progress-rate-limit
progress-stop-after-completionMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-06-18/progress/progress-stop-after-completionn/a: catalogue marks it n/a

prompts

requirementlevelsidemcpx as servermcpx as client
prompts-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-declare-capabilityn/a: addresses the other side
prompts-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-list-changed-notifyn/a: addresses the other side
prompts-image-base64-mimeMUSTservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-image-base64-mimen/a: addresses the other side
prompts-audio-base64-mimeMUST (not bolded)servertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-audio-base64-mimen/a: addresses the other side
prompts-embedded-must-includeMUSTservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-embedded-must-includen/a: addresses the other side
prompts-validate-argsSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-validate-argsn/a: addresses the other side
prompts-respect-capability-negotiationSHOULDbothtested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-respect-capability-negotiationtested: conformance.TestFeaturesClient/2025-06-18/prompts/prompts-respect-capability-negotiation
prompts-validate-ioMUSTbothtested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-validate-io; mcpclient.TestValidatePromptResultcovered

prompts / errors

requirementlevelsidemcpx as servermcpx as client
prompts-errors-32602-32603SHOULDservertested: conformance.TestFeaturesServer/2025-06-18/prompts/prompts-errors-32602-32603n/a: addresses the other side

prompts / pagination

requirementlevelsidemcpx as servermcpx as client
prompts-client-paginateSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-06-18/prompts/prompts-client-paginate

resources

requirementlevelsidemcpx as servermcpx as client
resources-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-declare-capabilityn/a: addresses the other side
resources-subscribe-cap-meaning-legacy(definition)servertested: e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/http-subscribe-delivers-upstream-updates-on-the-get-stream@2025-03-26,2025-06-18,2025-11-25
e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/stdio-subscribe-delivers-upstream-updates-under-the-mcpx-uri@2024-11-05,2025-03-26,2025-06-18,2025-11-25
n/a: addresses the other side
resources-cap-features-independentMAYservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-cap-features-independentn/a: addresses the other side
resources-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-list-changed-notifyn/a: addresses the other side
resources-updated-may-be-subresource(note, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-06-18/resources/resources-updated-may-be-subresource
resources-https-only-if-client-fetchableSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-https-only-if-client-fetchablen/a: addresses the other side
resources-https-else-other-schemeSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-https-else-other-schemen/a: addresses the other side
resources-file-xdg-mime-mayMAYservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-file-xdg-mime-mayn/a: addresses the other side
resources-custom-scheme-rfc3986MUSTservertested: cli.TestMCPXResourceURIs/2025-06-18/resources/resources-custom-scheme-rfc3986n/a: addresses the other side
resources-sec-validate-urisMUSTservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-sec-validate-urisn/a: addresses the other side
resources-sec-access-controlSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-binary-encodedMUSTservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-sec-binary-encodedn/a: addresses the other side
resources-sec-permissionsSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-annotations-defs(definition)bothtested: conformance.TestFeaturesServer/2025-06-18/resources/resources-annotations-defsgap #207: mcpclient.Resource has no annotations field; they are dropped on parse

resources / errors

requirementlevelsidemcpx as servermcpx as client
resources-not-found-32002SHOULDservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-not-found-32002n/a: addresses the other side
resources-internal-32603SHOULDservertested: conformance.TestFeaturesServer/2025-06-18/resources/resources-internal-32603n/a: addresses the other side

resources / subscriptions

requirementlevelsidemcpx as servermcpx as client
resources-subscribe-rpc(optional)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-06-18/resources/resources-subscribe-rpc
resources-unsubscribe-follows-subscribeshould (lowercase, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-06-18/resources/resources-unsubscribe-follows-subscribe

roots

requirementlevelsidemcpx as servermcpx as client
roots-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/roots/roots-client-declare-capability-at-initialize
roots-client-listchanged-must-notifyMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/roots/roots-client-listchanged-must-notify
roots-uri-must-be-file-schemeMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-only-expose-permitted-rootsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-uris-path-traversalMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-implement-access-controlsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-accessibilityMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-handle-unavailable-rootsSHOULDservern/a: mcpx does not consume rootsn/a: addresses the other side
roots-server-respect-boundariesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-validate-paths-against-rootsSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-prompt-consent-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-provide-root-management-uiSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-accessibility-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-root-changesSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-check-capability-before-useSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-handle-list-changesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-cache-root-informationSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-return-jsonrpc-errorsSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/roots/roots-client-return-jsonrpc-errors

sampling

requirementlevelsidemcpx as servermcpx as client
sampling-human-in-loop-can-denySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/sampling/sampling-human-in-loop-can-deny
sampling-app-review-uiSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-view-edit-promptsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-review-responsesSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/sampling/sampling-client-declare-capability-at-initialize
sampling-client-may-sample-fewer-tokensMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-model-preferencesMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-modify-system-promptMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-includecontextMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-evaluated-in-orderMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-over-prioritiesSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-name-substringSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-may-map-providerMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-stopreason-custom-valuesMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/sampling/sampling-stopreason-custom-values
sampling-client-errors-user-rejectedSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/sampling/sampling-client-errors-user-rejected
sampling-client-user-approval-controlsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-both-validate-message-contentSHOULDbothtested: conformance.TestSamplingValidation/2025-06-18/sampling/sampling-both-validate-message-contenttested: conformance.TestSamplingValidation/2025-06-18/sampling/sampling-both-validate-message-content
sampling-client-respect-hintsSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-06-18/sampling/sampling-client-rate-limit
sampling-both-handle-sensitive-dataMUSTbothtested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
tested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28

subscriptions

requirementlevelsidemcpx as servermcpx as client
subscriptions-list-changed-should-notifySHOULDservertested: conformance.TestStatefulServer/2025-06-18/subscriptions/subscriptions-list-changed-should-notifyn/a: addresses the other side
subscriptions-resources-updated-only-if-subscribedshould (schema)servertested: conformance.TestStatefulServer/2025-06-18/subscriptions/subscriptions-resources-updated-only-if-subscribedn/a: addresses the other side

tools

requirementlevelsidemcpx as servermcpx as client
tools-human-in-loop-denySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-ui-expose-indicate-confirmSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-declare-capabilityn/a: addresses the other side
tools-list-changed-means-emit(definition)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-list-changed-means-emitn/a: addresses the other side
tools-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-list-changed-notifyn/a: addresses the other side
tools-annotations-untrustedMUSTclientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-annotations-hints-no-decisionsshould (lowercase, schema)clientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-result-iserror-for-tool-errorsSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-result-iserror-for-tool-errorsn/a: addresses the other side
tools-result-lookup-errors-protocolshould (lowercase)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-result-lookup-errors-protocoln/a: addresses the other side
tools-error-unknown-tool-protocol(classification)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-error-unknown-tool-protocoln/a: addresses the other side
tools-error-invalid-args-protocol(classification)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-error-invalid-args-protocoln/a: addresses the other side
tools-embedded-resource-mayMAYservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-embedded-resource-mayn/a: addresses the other side
tools-embedded-implies-resources-capSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-embedded-implies-resources-capn/a: addresses the other side
tools-resource-link-mayMAYservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-resource-link-mayn/a: addresses the other side
tools-resource-link-may-not-listed(note)bothtested: conformance.TestFeaturesServer/2025-06-18/tools/tools-resource-link-may-not-listedtested: conformance.TestFeaturesClient/2025-06-18/tools/tools-resource-link-may-not-listed
tools-structured-also-textSHOULDservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-structured-also-textn/a: addresses the other side
tools-structured-object-only(type, schema)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-structured-object-onlyn/a: addresses the other side
tools-output-schema-conformMUSTservertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-output-schema-conformn/a: addresses the other side
tools-output-schema-client-validateSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-output-schema-object-root(type, schema)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-output-schema-object-rootn/a: addresses the other side
tools-display-name-precedence(rule)clientn/a: addresses the other sidegap #207: tool title and annotations.title are carried but nothing that displays a tool uses them
tools-server-sec-validate-inputsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-client-secSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement

tools / json-schema

requirementlevelsidemcpx as servermcpx as client
tools-inputschema-type-object-rootREQUIRED (schema)servertested: conformance.TestFeaturesServer/2025-06-18/tools/tools-inputschema-type-object-rootn/a: addresses the other side

transport-http-sse

requirementlevelsidemcpx as servermcpx as client
http-sse-server-backcompat-host-bothshould (lowercase)servern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-client-backcompat-probe-any-4xxshould (lowercase)clientn/a: addresses the other sidetested: pool.TestHTTPSSEFallback/2025-11-25/transport-http-sse/client-falls-back-to-get-endpoint-and-caches-it@2025-03-26,2025-06-18

transport-stdio

requirementlevelsidemcpx as servermcpx as client
transport-client-should-support-stdioSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-stdio/transport-client-should-support-stdio
transport-custom-preserve-jsonrpc-and-lifecycleMUSTbothn/a: mcpx serves MCP only over stdio and Streamable HTTP; its /v1 REST surface is not an MCP transportn/a: catalogue marks it n/a
transport-custom-document-patternsSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
stdio-no-embedded-newlinesMUST NOTbothtested: conformance.TestTransportServer/2025-06-18/transport-stdio/stdio-no-embedded-newlinestested: conformance.TestTransportClient/2025-06-18/transport-stdio/stdio-no-embedded-newlines
stdio-message-is-singleMUST (implied)bothtested: conformance.TestTransportServer/2025-06-18/transport-stdio/stdio-message-is-singletested: conformance.TestTransportClient/2025-06-18/transport-stdio/stdio-message-is-single
stdio-server-may-log-to-stderrMAYservertested: conformance.TestTransportServer/2025-06-18/transport-stdio/stdio-server-may-log-to-stderrn/a: addresses the other side
stdio-client-may-capture-stderrMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-stdio/stdio-client-may-capture-stderr
stdio-server-stdout-only-mcpMUST NOTservertested: conformance.TestTransportServer/2025-06-18/transport-stdio/stdio-server-stdout-only-mcpn/a: addresses the other side
stdio-client-stdin-only-mcpMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-stdio/stdio-client-stdin-only-mcp

transport-streamable-http

requirementlevelsidemcpx as servermcpx as client
streamable-http-server-single-endpoint-post-getMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-single-endpoint-post-getn/a: addresses the other side
streamable-http-server-validate-originMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-validate-originn/a: addresses the other side
streamable-http-server-bind-localhostSHOULDservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-bind-localhostn/a: addresses the other side
streamable-http-server-authenticateSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-post-every-messageMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-post-every-message
streamable-http-client-accept-bothMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-accept-both
streamable-http-post-body-singleMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-post-body-single
streamable-http-server-202-for-non-requestMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-202-for-non-requestn/a: addresses the other side
streamable-http-server-error-status-if-rejectedMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-error-status-if-rejectedn/a: addresses the other side
streamable-http-server-json-or-sse-for-requestMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-json-or-sse-for-requestn/a: addresses the other side
streamable-http-client-supports-json-and-sseMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-supports-json-and-sse
streamable-http-sse-includes-responseSHOULDservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-sse-includes-responsen/a: addresses the other side
streamable-http-post-sse-may-carry-requestsMAYservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-post-sse-may-carry-requestsn/a: addresses the other side
streamable-http-sse-not-closed-before-responseSHOULD NOTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-sse-not-closed-before-responsen/a: addresses the other side
streamable-http-server-terminates-sse-after-responseSHOULDservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-terminates-sse-after-responsen/a: addresses the other side
streamable-http-disconnect-not-cancelSHOULD NOTbothtested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-disconnect-not-canceln/a: catalogue marks it n/a
streamable-http-client-cancel-by-notificationSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-cancel-by-notification
streamable-http-server-may-resumeMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-may-getMAYclientn/a: addresses the other siden/a: optional feature mcpx does not implement
streamable-http-get-accept-sseMUSTclientn/a: addresses the other siden/a: no GET
streamable-http-get-sse-or-405MUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-get-sse-or-405n/a: addresses the other side
streamable-http-get-stream-unrelatedSHOULDservern/a: 405n/a: addresses the other side
streamable-http-get-no-responsesMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-get-close-any-timeMAYbothtested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-get-close-any-timetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-get-close-any-time
streamable-http-multiple-streamsMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-multiple-streams
streamable-http-no-broadcastMUST / MUST NOTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-no-broadcastn/a: addresses the other side
streamable-http-event-id-uniqueMUSTservern/a: no idsn/a: addresses the other side
streamable-http-client-resume-with-last-event-idSHOULDclientn/a: addresses the other sidetested: mcpclient.TestLegacyHTTPStreamResumption/2025-11-25/transport/client-resumes-closed-stream-with-last-event-id-after-retry@2025-03-26,2025-06-18,2025-11-25
streamable-http-server-no-cross-stream-replayMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-server-may-assign-sessionMAYservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-may-assign-sessionn/a: addresses the other side
streamable-http-session-id-secureSHOULDservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-session-id-securen/a: addresses the other side
streamable-http-session-id-visible-asciiMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-session-id-visible-asciin/a: addresses the other side
streamable-http-client-echoes-sessionMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-echoes-session
streamable-http-server-400-missing-sessionSHOULDservern/a: mcpx does not require one (stateless fallbackn/a: addresses the other side
streamable-http-server-404-terminated-sessionMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-404-terminated-sessionn/a: addresses the other side
streamable-http-client-reinit-on-404MUSTclientn/a: addresses the other sidetested: mcpclient.TestLegacySession404Recovery/2025-11-25/transport/client-reinitializes-on-session-404-and-retries-once@2025-03-26,2025-06-18,2025-11-25
streamable-http-client-delete-sessionSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-client-delete-session
streamable-http-server-may-405-deleteMAYservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-may-405-deleten/a: addresses the other side
streamable-http-protocol-version-header-is-negotiatedSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-06-18/transport-streamable-http/streamable-http-protocol-version-header-is-negotiated
streamable-http-server-assumes-2025-03-26SHOULDservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-assumes-2025-03-26n/a: addresses the other side
streamable-http-server-400-unsupported-version-headerMUSTservertested: conformance.TestTransportServer/2025-06-18/transport-streamable-http/streamable-http-server-400-unsupported-version-headern/a: addresses the other side

versioning

requirementlevelsidemcpx as servermcpx as client
lifecycle-client-sends-supported-versionMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/versioning/lifecycle-client-sends-supported-version
lifecycle-client-sends-latest-versionSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/versioning/lifecycle-client-sends-latest-version
lifecycle-client-may-support-olderMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/versioning/lifecycle-client-may-support-older
lifecycle-server-echoes-supported-versionMUSTservertested: conformance.TestLifecycleServer/2025-06-18/versioning/lifecycle-server-echoes-supported-versionn/a: addresses the other side
lifecycle-server-counteroffers-versionMUSTservertested: conformance.TestLifecycleServer/2025-06-18/versioning/lifecycle-server-counteroffers-versionn/a: addresses the other side
lifecycle-server-counteroffers-latestSHOULDservertested: conformance.TestLifecycleServer/2025-06-18/versioning/lifecycle-server-counteroffers-latestn/a: addresses the other side
lifecycle-client-disconnects-on-unsupported-versionSHOULD (prose) / MUST (schema)clientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/versioning/lifecycle-client-disconnects-on-unsupported-version
lifecycle-http-protocol-version-headerMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-06-18/versioning/lifecycle-http-protocol-version-header

2025-11-25

(security; not in area list)

requirementlevelsidemcpx as servermcpx as client
security-implementors-guidelinesSHOULDbothtested: conformance.TestMessagesServer/2025-11-25/security/security-implementors-guidelinesn/a: catalogue marks it n/a

authorization

requirementlevelsidemcpx as servermcpx as client
authorization-http-should-conform-stdio-should-notSHOULD / SHOULD NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authorization-custom-auth-mayMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-optionalOPTIONALbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-http-should-conformSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-stdio-use-environmentSHOULD NOTbothgap not implemented: #1975gap not implemented: #1950
authz-alt-transport-best-practiceMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-as-implements-oauth21MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-dcr-may-deprecatedMAYclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-bearer-headerMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-token-every-requestMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-no-token-in-queryMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-server-validates-tokenMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-validates-audienceMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-client-sends-only-own-as-tokensMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-accept-only-own-tokensMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-no-token-passthroughMUST NOTservergap not implemented: #1975n/a: addresses the other side
authz-error-status-codesMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-prm-rfc9728MUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-prm-lists-authorization-serversMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-as-metadata-rfc8414-or-oidcMUSTbothn/a: catalogue marks it n/an/a: optional feature mcpx does not implement
authz-prm-discovery-header-or-well-knownMUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-client-parses-www-authenticateMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-client-as-discovery-orderMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-resource-indicatorMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-canonical-uri-guidanceSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-secure-token-storageMUSTbothn/a: catalogue marks it n/agap not implemented: #1950
authz-as-short-lived-and-rotateSHOULD / MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-https-and-redirect-urisMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-pkce-verify-s256MUSTclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-open-redirectionMUST / SHOULDclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-proxy-consent-per-clientMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-registration-prioritySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-cimdSHOULD / MUSTclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-preregistrationSHOULDclientn/a: addresses the other siden/a: no OAuth client
authz-scope-in-challengeSHOULD / MUST / MUST NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-scope-selection-orderSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-insufficient-scope-403SHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-step-upSHOULD / MAYclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-cimd-as-securityMUST / SHOULDauth-servern/a: addresses the other siden/a: addresses the other side
authz-follow-security-considerationsMUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement

cancellation

requirementlevelsidemcpx as servermcpx as client
cancellation-ref-same-direction-in-progressMUSTbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-ref-same-direction-in-progresstested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-ref-same-direction-in-progress
cancellation-client-must-not-cancel-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-client-must-not-cancel-initialize
cancellation-task-use-tasks-cancelMUSTbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-task-use-tasks-canceltested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-task-use-tasks-cancel
cancellation-requestid-required-non-task-forbidden-taskMUST / MUST NOTbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-requestid-required-non-task-forbidden-tasktested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-requestid-required-non-task-forbidden-task
cancellation-receiver-should-stop-free-no-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-receiver-should-stop-free-no-responsetested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-receiver-should-stop-free-no-response
cancellation-receiver-may-ignoreMAYbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-receiver-may-ignoretested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-receiver-may-ignore
cancellation-sender-ignore-late-responseSHOULDbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-sender-ignore-late-responsetested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-sender-ignore-late-response
cancellation-handle-racesMUSTbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-handle-racestested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-handle-races
cancellation-log-reasonsSHOULDbothgap #77: a cancellation reason reaches Server.OnCancel, which nothing outside tests sets; it is never loggedtested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-log-reasons
cancellation-ui-indicateSHOULDbothn/a: no UIn/a: no UI
cancellation-ignore-invalidSHOULDbothtested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-ignore-invalidtested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-ignore-invalid
cancellation-processing-should-ceaseSHOULDboth (receiver)tested: conformance.TestUtilitiesServer/2025-11-25/cancellation/cancellation-processing-should-ceasetested: conformance.TestUtilitiesClient/2025-11-25/cancellation/cancellation-processing-should-cease

completion

requirementlevelsidemcpx as servermcpx as client
completion-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-11-25/completion/completion-declare-capabilityn/a: addresses the other side
completion-max-100descr. + schemaservertested: conformance.TestUtilitiesServer/2025-11-25/completion/completion-max-100n/a: addresses the other side
completion-server-shouldSHOULDservertested: daemon.TestHandleCompleteRateLimitHotChangeAndZeroDisables@all
daemon.TestHandleCompleteRateLimitRefusesBeforePool@all
daemon.TestHandleCompleteServerWindowBindsAcrossCallers@all
daemon.TestLocalCompletionCapAfterRanking@all
daemon.TestLocalCompletionTiersPrompts@all
daemon.TestLocalCompletionTiersResources@all
n/a: addresses the other side
completion-client-shouldSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/completion/completion-client-should
completion-securityMUSTbothgap #1968: partial: completion inputs are validated and completion/complete is rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) since #2387, but there is no per-caller access control for sensitive suggestions and no completion-specific information-disclosure control; waiting on principals #1968 / permissive-auth #1972n/a: catalogue marks it n/a
completion-errorsSHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/completion/completion-errorsn/a: addresses the other side
completion-context-argumentsshould (lowercase)clientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/completion/completion-context-arguments

elicitation

requirementlevelsidemcpx as servermcpx as client
elicitation-server-no-sensitive-info-in-form-modeMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-no-sensitive-info-in-form-moden/a: addresses the other side
elicitation-server-sensitive-via-url-modeMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-sensitive-via-url-moden/a: addresses the other side
elicitation-client-show-requesting-serverMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-show-requesting-server
elicitation-client-form-review-modify-responsesMUSTclientn/a: addresses the other sidetested: cli.TestRenderAnswerReviewMasksSensitiveFields@2025-11-25,2026-07-28
cli.TestRenderQuestionFormShowsSchemaDefaults@2025-11-25,2026-07-28
elicitation-client-decline-cancel-optionsMUSTclientn/a: addresses the other sidetested: elicit.TestAnExpiredQuestionBecomesCancelNotDecline@2025-06-18,2025-11-25,2026-07-28
elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-url-show-domain-and-consentMUSTclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-declare-capability-at-initialize
elicitation-empty-capability-means-form-only(definition)bothtested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-empty-capability-means-form-onlytested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-empty-capability-means-form-only
elicitation-client-support-at-least-one-modeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-support-at-least-one-mode
elicitation-server-no-undeclared-modesMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-no-undeclared-modesn/a: addresses the other side
elicitation-request-must-include-mode-and-messageMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-request-must-include-mode-and-messagen/a: addresses the other side
elicitation-server-may-omit-mode-for-formMAYservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-may-omit-mode-for-formn/a: addresses the other side
elicitation-client-missing-mode-is-formMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-missing-mode-is-form
elicitation-form-request-shapeMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-form-request-shapen/a: addresses the other side
elicitation-form-schema-flat-primitivesMUST (schema-structural)servertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-form-schema-flat-primitivesn/a: addresses the other side
elicitation-client-prepopulate-defaultsSHOULDclientn/a: addresses the other sidetested: mcpclient.TestElicitationDeclarations/2025-11-25/elicitation/client-applies-schema-defaults
elicitation-url-request-shape-with-idMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-request-shape-with-idn/a: addresses the other side
elicitation-url-must-be-validMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-must-be-validn/a: addresses the other side
elicitation-client-elicitationid-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-elicitationid-opaque
elicitation-server-may-send-complete-notificationMAYservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-may-send-complete-notificationn/a: addresses the other side
elicitation-complete-only-to-initiating-clientMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-complete-only-to-initiating-clientn/a: addresses the other side
elicitation-complete-must-carry-idMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-complete-must-carry-idn/a: addresses the other side
elicitation-client-ignore-unknown-completeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-ignore-unknown-complete
elicitation-client-may-auto-retry-on-completeMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-may-auto-retry-on-complete
elicitation-client-manual-retry-cancel-controlsSHOULDclientn/a: addresses the other sidetested: e2e.TestElicitationsCanBeListedAndAnsweredFromTheCommandLine@2025-11-25,2026-07-28
elicitation-server-may-return-url-required-errorMAYservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-may-return-url-required-errorn/a: addresses the other side
elicitation-url-required-error-only-when-requiredMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-required-error-only-when-requiredn/a: addresses the other side
elicitation-url-required-error-lists-elicitationsMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-required-error-lists-elicitationsn/a: addresses the other side
elicitation-url-required-error-url-mode-with-idMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-required-error-url-mode-with-idn/a: addresses the other side
elicitation-server-must-return-32042MUSTservergap #214: a url question for a client that cannot take it stays with the broker; -32042 is never returnedn/a: addresses the other side
elicitation-client-32602-on-undeclared-modeMUSTclientn/a: addresses the other sidetested: mcpclient.TestElicitationDeclarations/2025-11-25/elicitation/undeclared-mode-is-32602
elicitation-server-state-bound-to-userMUSTservergap #257: mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sendsn/a: addresses the other side
elicitation-state-not-session-id-aloneMUST NOTservergap #257: state is bound to the request, not a user; there is no user to bind ton/a: addresses the other side
elicitation-state-storage-protectedMUSTservertested: elicit.TestOpenAtCreatesOwnerOnlyStore@2025-11-25,2026-07-28
elicit.TestOpenAtMigratesLegacyStore@2025-11-25,2026-07-28
n/a: addresses the other side
elicitation-remote-user-id-from-authorizationMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
elicitation-server-must-not-self-authorize-via-urlMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-must-not-self-authorize-via-urln/a: addresses the other side
elicitation-third-party-creds-not-via-clientMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-third-party-creds-not-via-clientn/a: addresses the other side
elicitation-no-token-passthroughMUST NOTservern/a: optional feature mcpx does not implementn/a: addresses the other side
elicitation-user-authorizes-server-directlyMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-user-authorizes-server-directlyn/a: addresses the other side
elicitation-server-not-transmit-url-creds-to-clientMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-not-transmit-url-creds-to-clientn/a: addresses the other side
elicitation-server-bind-to-client-and-userMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-server-bind-to-client-and-usern/a: addresses the other side
elicitation-client-user-approval-controlsSHOULDclientn/a: addresses the other sidetested: elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28
elicitation-client-allow-decline-any-timeSHOULDclientn/a: addresses the other sidetested: elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-rate-limit
elicitation-client-clear-what-and-whySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-clear-what-and-why
elicitation-client-validate-responsesSHOULDclientn/a: addresses the other sidetested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayed
elicitation-server-validate-received-dataSHOULDservertested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayedn/a: addresses the other side
elicitation-url-no-sensitive-user-infoMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-no-sensitive-user-infon/a: addresses the other side
elicitation-url-not-pre-authenticatedMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-not-pre-authenticatedn/a: addresses the other side
elicitation-no-clickable-urls-in-formSHOULD NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-no-clickable-urls-in-formn/a: addresses the other side
elicitation-url-https-outside-devSHOULDservertested: conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-https-outside-devn/a: addresses the other side
elicitation-client-handle-urls-carefullyMUSTclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-no-url-prefetchMUST NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-no-url-prefetch
elicitation-client-no-open-without-consentMUST NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-no-open-without-consent
elicitation-client-show-full-urlMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-show-full-url
elicitation-client-open-url-securelyMUSTclientn/a: addresses the other sidetested: elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28
elicitation-client-highlight-domainSHOULDclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
elicitation-client-warn-punycodeSHOULDclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-no-clickable-except-url-fieldSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-no-clickable-except-url-field
elicitation-server-no-trust-client-user-idMUST NOTservergap #257: mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sendsn/a: addresses the other side
elicitation-server-follow-best-practicesSHOULDservergap #255: requestState signing is sound and question storage is owner-only (0700/0600, #1676); there is no user identityn/a: addresses the other side
elicitation-url-verify-opening-userMUSTservern/a: mcpx issues no URL; the upstream that does sees mcpx’s identity, not the end user’sn/a: addresses the other side
elicitation-url-same-user-start-and-completeMUSTservern/a: see Implied: shared pooled upstreamn/a: addresses the other side
elicitation-url-identity-resilient-to-url-tamperingMUSTservern/a: catalogue marks it n/an/a: addresses the other side
elicitation-task-related-id-sharedMUSTservertested: conformance.TestStatefulServer/2025-11-25/elicitation/elicitation-task-related-id-sharedn/a: addresses the other side

errors

requirementlevelsidemcpx as servermcpx as client
messages-error-response-same-id-unless-unreadableMUSTbothtested: conformance.TestMessagesServer/2025-11-25/errors/messages-error-response-same-id-unless-unreadabletested: conformance.TestMessagesClient/2025-11-25/errors/messages-error-response-same-id-unless-unreadable
errors-error-has-code-and-messageMUSTbothtested: conformance.TestMessagesServer/2025-11-25/errors/errors-error-has-code-and-messagetested: conformance.TestMessagesClient/2025-11-25/errors/errors-error-has-code-and-message
errors-codes-integersMUSTbothtested: conformance.TestMessagesServer/2025-11-25/errors/errors-codes-integerstested: conformance.TestMessagesClient/2025-11-25/errors/errors-codes-integers
errors-message-concise-single-sentenceSHOULDbothtested: conformance.TestMessagesServer/2025-11-25/errors/errors-message-concise-single-sentencen/a: sends no errors except to server requests

json-schema

requirementlevelsidemcpx as servermcpx as client
json-schema-default-2020-12(definitional)bothtested: conformance.TestMessagesServer/2025-11-25/json-schema/json-schema-default-2020-12n/a: catalogue marks it n/a
json-schema-may-declare-dialectMAYbothtested: conformance.TestMessagesServer/2025-11-25/json-schema/json-schema-may-declare-dialecttested: conformance.TestMessagesClient/2025-11-25/json-schema/json-schema-may-declare-dialect
json-schema-support-2020-12MUSTbothtested: mcpclient.TestCallToolRejectsInvalidArgumentsBeforeTheTransport@alln/a: passes through
json-schema-validate-per-dialect-and-error-on-unsupportedMUSTbothtested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
tested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
json-schema-document-dialectsSHOULDbothtested: schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
tested: schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
json-schema-recommend-2020-12RECOMMENDEDbothtested: conformance.TestMessagesServer/2025-11-25/json-schema/json-schema-recommend-2020-12n/a: catalogue marks it n/a
json-schema-schemas-validMUSTbothtested: conformance.TestMessagesServer/2025-11-25/json-schema/json-schema-schemas-validn/a: catalogue marks it n/a
tools-schema-default-2020-12(definition)bothtested: conformance.TestMessagesServer/2025-11-25/json-schema/tools-schema-default-2020-12tested: conformance.TestMessagesClient/2025-11-25/json-schema/tools-schema-default-2020-12

lifecycle

requirementlevelsidemcpx as servermcpx as client
lifecycle-implementations-support-base-and-lifecycleMUSTbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-implementations-support-base-and-lifecycletested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-implementations-support-base-and-lifecycle
lifecycle-initialization-first-interactionMUSTbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-initialization-first-interactiontested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-initialization-first-interaction
lifecycle-client-sends-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-client-sends-initialize
lifecycle-server-responds-caps-and-infoMUSTservertested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-server-responds-caps-and-infon/a: addresses the other side
lifecycle-client-sends-initialized-after-initMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-client-sends-initialized-after-init
lifecycle-client-no-requests-before-init-responseSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-client-no-requests-before-init-response
lifecycle-server-no-requests-before-initializedSHOULD NOTservertested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-server-no-requests-before-initializedn/a: addresses the other side
lifecycle-capabilities-establish-session-features(definitional)bothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-capabilities-establish-session-featurestested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-capabilities-establish-session-features
lifecycle-respect-negotiated-version-and-capabilities-mustMUSTbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-musttested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-respect-negotiated-version-and-capabilities-must
lifecycle-stdio-client-shutdown-sequenceSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-stdio-client-shutdown-sequence
lifecycle-stdio-server-may-exitMAYservertested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-stdio-server-may-exitn/a: addresses the other side
lifecycle-http-shutdown-closing-connection(definitional)bothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-http-shutdown-closing-connectiontested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-http-shutdown-closing-connection
lifecycle-prepared-for-error-casesSHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-prepared-for-error-casestested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-prepared-for-error-cases
lifecycle-timeouts-establishSHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-timeouts-establishtested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-timeouts-establish
lifecycle-timeouts-cancel-on-expirySHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-timeouts-cancel-on-expirytested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-timeouts-cancel-on-expiry
lifecycle-timeouts-per-request-configurableSHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-timeouts-per-request-configurabletested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-timeouts-per-request-configurable
lifecycle-timeouts-may-reset-on-progressMAYbothn/a: catalogue marks it n/atested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-timeouts-may-reset-on-progress
lifecycle-timeouts-max-regardless-of-progressSHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-timeouts-max-regardless-of-progresstested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-timeouts-max-regardless-of-progress
lifecycle-cancel-not-initializeMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-cancel-not-initialize
lifecycle-instructions-may-go-in-system-promptMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-instructions-may-go-in-system-prompt
lifecycle-capability-sets-open(definitional)bothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-capability-sets-opentested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-capability-sets-open
lifecycle-implementation-name-version-requiredREQUIRED (schema field)bothtested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-implementation-name-version-requiredtested: conformance.TestLifecycleClient/2025-11-25/lifecycle/lifecycle-implementation-name-version-required
lifecycle-include-context-only-if-declaredSHOULDservertested: conformance.TestLifecycleServer/2025-11-25/lifecycle/lifecycle-include-context-only-if-declaredn/a: addresses the other side

logging

requirementlevelsidemcpx as servermcpx as client
logging-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-declare-capabilityn/a: addresses the other side
logging-client-may-setlevelMAYclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/logging/logging-client-may-setlevel
logging-setlevel-at-or-aboveshould (schema)servertested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-setlevel-at-or-aboven/a: addresses the other side
logging-unset-server-choosesMAYservertested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-unset-server-choosesn/a: addresses the other side
logging-errors-setlevelSHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-errors-setleveln/a: addresses the other side
logging-server-hygieneSHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-server-hygienen/a: addresses the other side
logging-no-secrets-piiMUST NOTboth (emitter)tested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-no-secrets-piitested: conformance.TestUtilitiesClient/2025-11-25/logging/logging-no-secrets-pii
logging-impl-validate-controlSHOULDbothtested: conformance.TestUtilitiesServer/2025-11-25/logging/logging-impl-validate-controltested: conformance.TestUtilitiesClient/2025-11-25/logging/logging-impl-validate-control

messages

requirementlevelsidemcpx as servermcpx as client
messages-all-messages-follow-jsonrpc-2MUSTbothtested: conformance.TestJSONRPCFramesEveryRevision/2025-11-25/messages/all-messages-follow-jsonrpc-2-server-http
conformance.TestJSONRPCFramesEveryRevision/2025-11-25/messages/all-messages-follow-jsonrpc-2-server-stdio
conformance.TestMessagesServer/2025-11-25/messages/messages-all-messages-follow-jsonrpc-2
tested: conformance.TestJSONRPCFramesEveryRevision/2025-11-25/messages/all-messages-follow-jsonrpc-2-client
messages-other-components-mayMAYbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-other-components-maytested: conformance.TestMessagesClient/2025-11-25/messages/messages-other-components-may
messages-request-id-string-or-integerMUSTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-request-id-string-or-integertested: conformance.TestMessagesClient/2025-11-25/messages/messages-request-id-string-or-integer
messages-request-id-not-nullMUST NOTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-request-id-not-nulltested: conformance.TestMessagesClient/2025-11-25/messages/messages-request-id-not-null
messages-request-id-unique-per-sessionMUST NOTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-request-id-unique-per-sessiontested: conformance.TestMessagesClient/2025-11-25/messages/messages-request-id-unique-per-session
messages-result-response-same-idMUSTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-result-response-same-idtested: conformance.TestMessagesClient/2025-11-25/messages/messages-result-response-same-id
messages-result-response-has-resultMUSTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-result-response-has-resulttested: conformance.TestMessagesClient/2025-11-25/messages/messages-result-response-has-result
messages-result-any-objectMAYbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-result-any-objecttested: conformance.TestMessagesClient/2025-11-25/messages/messages-result-any-object
messages-notification-no-responseMUST NOTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-notification-no-responsetested: conformance.TestMessagesClient/2025-11-25/messages/messages-notification-no-response
messages-notification-no-idMUST NOTbothtested: conformance.TestMessagesServer/2025-11-25/messages/messages-notification-no-idtested: conformance.TestMessagesClient/2025-11-25/messages/messages-notification-no-id
transport-jsonrpc-utf8MUSTbothtested: conformance.TestMessagesServer/2025-11-25/messages/transport-jsonrpc-utf8tested: conformance.TestMessagesClient/2025-11-25/messages/transport-jsonrpc-utf8

meta

requirementlevelsidemcpx as servermcpx as client
meta-no-assumptions-on-reserved-keysMUST NOTbothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-no-assumptions-on-reserved-keystested: conformance.TestMessagesClient/2025-11-25/meta/meta-no-assumptions-on-reserved-keys
meta-prefix-label-formatMUSTbothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-prefix-label-formattested: conformance.TestMessagesClient/2025-11-25/meta/meta-prefix-label-format
meta-reverse-dns-shouldSHOULDbothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-reverse-dns-shouldtested: conformance.TestMessagesClient/2025-11-25/meta/meta-reverse-dns-should
meta-reserved-prefix-second-label(reservation)bothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-reserved-prefix-second-labeltested: conformance.TestMessagesClient/2025-11-25/meta/meta-reserved-prefix-second-label
meta-name-formatMUST / MAYbothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-name-formattested: conformance.TestMessagesClient/2025-11-25/meta/meta-name-format
meta-schema-may-reserve-names(reservation)bothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-schema-may-reserve-namestested: conformance.TestMessagesClient/2025-11-25/meta/meta-schema-may-reserve-names
meta-progress-token-receiver-not-obligated(definitional)bothtested: conformance.TestMessagesServer/2025-11-25/meta/meta-progress-token-receiver-not-obligatedtested: conformance.TestMessagesClient/2025-11-25/meta/meta-progress-token-receiver-not-obligated
meta-icons-client-mime-supportMUST / SHOULDclientn/a: addresses the other siden/a: renders no icons
meta-icons-consumer-securityMUSTclientn/a: addresses the other siden/a: never fetches icons; passes metadata through
meta-icons-reject-unsafe-schemesMUSTclientn/a: addresses the other siden/a: does not dereference). Downstream host must do it
meta-icons-may-limit-sizeMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
meta-icons-same-originSHOULDclientn/a: addresses the other siden/a: catalogue marks it n/a

pagination

requirementlevelsidemcpx as servermcpx as client
pagination-no-fixed-page-sizeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/pagination/pagination-no-fixed-page-size
pagination-stable-cursorsSHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/pagination/pagination-stable-cursorsn/a: addresses the other side
pagination-invalid-cursor-gracefulSHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/pagination/pagination-invalid-cursor-gracefuln/a: addresses the other side
pagination-missing-nextcursor-endSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/pagination/pagination-missing-nextcursor-end
pagination-support-both-flowsSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/pagination/pagination-support-both-flows
pagination-cursor-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/pagination/pagination-cursor-opaque
pagination-no-persist-across-sessionsMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2025-11-25/pagination/pagination-no-persist-across-sessions
pagination-invalid-cursor-32602SHOULDservertested: conformance.TestUtilitiesServer/2025-11-25/pagination/pagination-invalid-cursor-32602n/a: addresses the other side

ping

requirementlevelsidemcpx as servermcpx as client
ping-receiver-responds-promptly-emptyMUSTbothtested: conformance.TestLifecycleServer/2025-11-25/ping/ping-receiver-responds-promptly-emptytested: conformance.TestLifecycleClient/2025-11-25/ping/ping-receiver-responds-promptly-empty
ping-either-party-may-initiate(definitional)bothtested: conformance.TestLifecycleServer/2025-11-25/ping/ping-either-party-may-initiatetested: conformance.TestLifecycleClient/2025-11-25/ping/ping-either-party-may-initiate
ping-sender-may-consider-staleMAYbothn/a: catalogue marks it n/an/a: does not ping
ping-periodic-pingsSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
ping-frequency-configurableSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-timeouts-appropriateSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-avoid-excessiveSHOULDbothtested: conformance.TestLifecycleServer/2025-11-25/ping/ping-avoid-excessivetested: conformance.TestLifecycleClient/2025-11-25/ping/ping-avoid-excessive
ping-timeout-is-connection-failureSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-multiple-failures-may-resetMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-log-failuresSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
ping-schema-receiver-must-respondmust (lowercase)bothtested: conformance.TestLifecycleServer/2025-11-25/ping/ping-schema-receiver-must-respondtested: conformance.TestLifecycleClient/2025-11-25/ping/ping-schema-receiver-must-respond

progress

requirementlevelsidemcpx as servermcpx as client
progress-token-string-or-intMUSTboth (requester)n/a: as a server mcpx never requests progress; a host’s token is relayed back verbatimtested: conformance.TestUtilitiesClient/2025-11-25/progress/progress-token-string-or-int
progress-token-unique-senderMUSTbothtested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-token-unique-sendertested: conformance.TestUtilitiesClient/2025-11-25/progress/progress-token-unique-sender
progress-receiver-may-sendMAYbothn/a: optional feature mcpx does not implementn/a: never receives requests with tokens
progress-must-increaseMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-must-increasen/a: catalogue marks it n/a
progress-message-human-readableSHOULDboth (notifier)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-message-human-readablen/a: catalogue marks it n/a
progress-only-active-tokensMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-only-active-tokensn/a: catalogue marks it n/a
progress-receiver-may-skipMAYbothtested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-receiver-may-skiptested: conformance.TestUtilitiesClient/2025-11-25/progress/progress-receiver-may-skip
progress-task-token-lives-with-taskMUSTboth (receiver)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-task-token-lives-with-taskn/a: catalogue marks it n/a
progress-task-same-tokenMUSTboth (receiver)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-task-same-tokenn/a: catalogue marks it n/a
progress-task-stop-at-terminalMUSTboth (receiver)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-task-stop-at-terminaln/a: catalogue marks it n/a
progress-track-tokensSHOULDbothn/a: catalogue marks it n/atested: conformance.TestUtilitiesClient/2025-11-25/progress/progress-track-tokens
progress-rate-limitSHOULDbothtested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-rate-limittested: conformance.TestUtilitiesClient/2025-11-25/progress/progress-rate-limit
progress-stop-after-completionMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2025-11-25/progress/progress-stop-after-completionn/a: catalogue marks it n/a

prompts

requirementlevelsidemcpx as servermcpx as client
prompts-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-declare-capabilityn/a: addresses the other side
prompts-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-list-changed-notifyn/a: addresses the other side
prompts-image-base64-mimeMUSTservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-image-base64-mimen/a: addresses the other side
prompts-audio-base64-mimeMUST (not bolded)servertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-audio-base64-mimen/a: addresses the other side
prompts-embedded-must-includeMUSTservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-embedded-must-includen/a: addresses the other side
prompts-validate-argsSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-validate-argsn/a: addresses the other side
prompts-respect-capability-negotiationSHOULDbothtested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-respect-capability-negotiationtested: conformance.TestFeaturesClient/2025-11-25/prompts/prompts-respect-capability-negotiation
prompts-validate-ioMUSTbothtested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-validate-io; mcpclient.TestValidatePromptResultcovered

prompts / errors

requirementlevelsidemcpx as servermcpx as client
prompts-errors-32602-32603SHOULDservertested: conformance.TestFeaturesServer/2025-11-25/prompts/prompts-errors-32602-32603n/a: addresses the other side

prompts / pagination

requirementlevelsidemcpx as servermcpx as client
prompts-client-paginateSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/prompts/prompts-client-paginate

resources

requirementlevelsidemcpx as servermcpx as client
resources-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-declare-capabilityn/a: addresses the other side
resources-subscribe-cap-meaning-legacy(definition)servertested: e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/http-subscribe-delivers-upstream-updates-on-the-get-stream@2025-03-26,2025-06-18,2025-11-25
e2e.TestResourceSubscriptionsReachUpstream/2025-11-25/resources/stdio-subscribe-delivers-upstream-updates-under-the-mcpx-uri@2024-11-05,2025-03-26,2025-06-18,2025-11-25
n/a: addresses the other side
resources-cap-features-independentMAYservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-cap-features-independentn/a: addresses the other side
resources-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-list-changed-notifyn/a: addresses the other side
resources-updated-may-be-subresource(note, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/resources/resources-updated-may-be-subresource
resources-https-only-if-client-fetchableSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-https-only-if-client-fetchablen/a: addresses the other side
resources-https-else-other-schemeSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-https-else-other-schemen/a: addresses the other side
resources-file-xdg-mime-mayMAYservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-file-xdg-mime-mayn/a: addresses the other side
resources-custom-scheme-rfc3986MUSTservertested: cli.TestMCPXResourceURIs/2025-11-25/resources/resources-custom-scheme-rfc3986n/a: addresses the other side
resources-sec-validate-urisMUSTservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-sec-validate-urisn/a: addresses the other side
resources-sec-access-controlSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-binary-encodedMUSTservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-sec-binary-encodedn/a: addresses the other side
resources-sec-permissionsSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-annotations-defs(definition)bothtested: conformance.TestFeaturesServer/2025-11-25/resources/resources-annotations-defsgap #207: mcpclient.Resource has no annotations field; they are dropped on parse

resources / errors

requirementlevelsidemcpx as servermcpx as client
resources-not-found-32002SHOULDservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-not-found-32002n/a: addresses the other side
resources-internal-32603SHOULDservertested: conformance.TestFeaturesServer/2025-11-25/resources/resources-internal-32603n/a: addresses the other side

resources / subscriptions

requirementlevelsidemcpx as servermcpx as client
resources-subscribe-rpc(optional)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/resources/resources-subscribe-rpc
resources-unsubscribe-follows-subscribeshould (lowercase, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/resources/resources-unsubscribe-follows-subscribe

roots

requirementlevelsidemcpx as servermcpx as client
roots-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/roots/roots-client-declare-capability-at-initialize
roots-client-listchanged-must-notifyMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/roots/roots-client-listchanged-must-notify
roots-uri-must-be-file-schemeMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-only-expose-permitted-rootsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-uris-path-traversalMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-implement-access-controlsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-accessibilityMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-handle-unavailable-rootsSHOULDservern/a: mcpx does not consume rootsn/a: addresses the other side
roots-server-respect-boundariesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-validate-paths-against-rootsSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-prompt-consent-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-provide-root-management-uiSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-accessibility-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-root-changesSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-check-capability-before-useSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-handle-list-changesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-cache-root-informationSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-return-jsonrpc-errorsSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/roots/roots-client-return-jsonrpc-errors

sampling

requirementlevelsidemcpx as servermcpx as client
sampling-human-in-loop-can-denySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-human-in-loop-can-deny
sampling-app-review-uiSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-view-edit-promptsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-review-responsesSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-client-declare-capability-at-initializeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-client-declare-capability-at-initialize
sampling-client-declare-tools-capabilityMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-client-declare-tools-capability
sampling-server-no-tools-without-capabilityMUST NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-server-no-tools-without-capabilityn/a: addresses the other side
sampling-client-error-on-tools-without-capabilityMUSTclientn/a: addresses the other sidetested: mcpclient.TestSamplingDeclarations/2025-11-25/sampling/tools-without-capability-is-32602
sampling-server-includecontext-only-with-context-capabilitySHOULD NOTservertested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-server-includecontext-only-with-context-capabilityn/a: addresses the other side
sampling-server-avoid-includecontext-valuesSHOULDservergap #210: an upstream sampling request is relayed as-is: tools and includeContext reach a client that did not declare sampling.tools or sampling.contextn/a: addresses the other side
sampling-tool-result-message-only-tool-resultsMUSTbothtested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-tool-result-message-only-tool-resultstested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-tool-result-message-only-tool-results
sampling-tool-use-result-balanceMUSTbothtested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-tool-use-result-balancetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-tool-use-result-balance
sampling-server-answer-every-tooluseMUSTservertested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-server-answer-every-toolusen/a: addresses the other side
sampling-toolresult-id-must-match-tooluseMUSTbothtested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-toolresult-id-must-match-toolusetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-toolresult-id-must-match-tooluse
sampling-toolchoice-required-model-must-use-toolMUSTclientn/a: addresses the other siden/a: mcpx never declares tools
sampling-toolchoice-none-model-must-not-use-toolsMUST NOTclientn/a: addresses the other siden/a: catalogue marks it n/a
sampling-parallel-tool-toggle-extension-onlyMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
sampling-tooluse-meta-preserveSHOULDclient (text says “Clients”; see Conflicts)n/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-tooluse-meta-preserve
sampling-toolresult-structured-conform-outputschemaSHOULDbothtested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-toolresult-structured-conform-outputschematested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-toolresult-structured-conform-outputschema
sampling-client-may-sample-fewer-tokensMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-model-preferencesMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-modify-system-promptMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-includecontextMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-evaluated-in-orderMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-over-prioritiesSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-name-substringSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-may-map-providerMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-stopreason-custom-valuesMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-stopreason-custom-values
sampling-client-errors-user-rejected-and-tool-shapeSHOULDclientn/a: addresses the other sidegap #1836: only the -32602 content half remains: sampling messages/results are not yet checked for a missing tool result or tool results mixed with other content (Phase 2 of #1834); the -1 half is covered – a refusal reaches the upstream as -1 (typed CodedError, #1835)
sampling-client-user-approval-controlsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-both-validate-message-contentSHOULDbothtested: conformance.TestSamplingValidation/2025-11-25/sampling/sampling-both-validate-message-contenttested: conformance.TestSamplingValidation/2025-11-25/sampling/sampling-both-validate-message-content
sampling-client-respect-hintsSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-client-rate-limit
sampling-both-handle-sensitive-dataMUSTbothtested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
tested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
sampling-both-iteration-limits-tool-loopsSHOULDbothtested: conformance.TestClientFeaturesServer/2025-11-25/sampling/sampling-both-iteration-limits-tool-loopstested: conformance.TestClientFeaturesClient/2025-11-25/sampling/sampling-both-iteration-limits-tool-loops

subscriptions

requirementlevelsidemcpx as servermcpx as client
subscriptions-list-changed-should-notifySHOULDservertested: conformance.TestStatefulServer/2025-11-25/subscriptions/subscriptions-list-changed-should-notifyn/a: addresses the other side
subscriptions-resources-updated-only-if-subscribedshould (schema)servertested: conformance.TestStatefulServer/2025-11-25/subscriptions/subscriptions-resources-updated-only-if-subscribedn/a: addresses the other side

tasks

requirementlevelsidemcpx as servermcpx as client
tasks-declare-capabilityMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-declare-capabilityn/a: optional feature mcpx does not implement
tasks-augment-only-if-declaredSHOULDboth (requestor)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-augment-only-if-declaredtested: conformance.TestStatefulClient/2025-11-25/tasks/tasks-augment-only-if-declared
tasks-no-capability-no-tasksSHOULD NOTboth (requestor)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-no-capability-no-taskstested: conformance.TestStatefulClient/2025-11-25/tasks/tasks-no-capability-no-tasks
tasks-tool-no-cap-no-augmentMUST NOTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2025-11-25/tasks/tasks-tool-no-cap-no-augment
tasks-tool-forbidden-defaultMUST NOT / SHOULDbothtested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-tool-forbidden-defaultn/a: catalogue marks it n/a
tasks-tool-optionalMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
tasks-tool-requiredMUSTbothn/a: never requiredn/a: mcpx reads upstream execution.taskSupport to serve its own clients, but never task-augments the upstream call; a required upstream tool that enforces it would fail
tasks-requestor-may-ttlMAYboth (requestor)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-requestor-may-ttln/a: catalogue marks it n/a
tasks-poll-respect-intervalSHOULDboth (requestor)n/a: catalogue marks it n/an/a: no upstream tasks
tasks-poll-until-terminalSHOULDboth (requestor)n/a: catalogue marks it n/an/a: catalogue marks it n/a
tasks-sse-client-may-disconnectMAY / MUSTclientn/a: addresses the other siden/a: catalogue marks it n/a
tasks-status-notification-optionalMAY / MUST NOTbothn/a: optional feature mcpx does not implementtested: conformance.TestStatefulClient/2025-11-25/tasks/tasks-status-notification-optional
tasks-status-notification-no-related-taskSHOULD NOTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-status-notification-no-related-taskn/a: catalogue marks it n/a
tasks-receiver-without-cap-ignoreMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-receiver-without-cap-ignoren/a: mcpx declares no client task caps; unverified whether it strips task from an upstream sampling/createMessage
tasks-receiver-may-requireMAYboth (receiver)n/a: catalogue marks it n/an/a: catalogue marks it n/a
tasks-id-string-receiver-uniqueMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-id-string-receiver-uniquen/a: catalogue marks it n/a
tasks-begin-workingMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-begin-workingn/a: catalogue marks it n/a
tasks-valid-transitionsMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-valid-transitionsn/a: catalogue marks it n/a
tasks-terminal-immutableMUST NOTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-terminal-immutablen/a: catalogue marks it n/a
tasks-input-required-moveSHOULDboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-input-required-moven/a: catalogue marks it n/a
tasks-input-request-related-taskMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-input-request-related-taskn/a: catalogue marks it n/a
tasks-requestor-input-required-call-resultSHOULDboth (requestor)n/a: catalogue marks it n/an/a: catalogue marks it n/a
tasks-leave-input-requiredSHOULDboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-leave-input-requiredn/a: catalogue marks it n/a
tasks-sse-any-streamSHOULDclientn/a: addresses the other siden/a: catalogue marks it n/a
tasks-sse-no-upgrade-getSHOULD NOTservertested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-sse-no-upgrade-getn/a: addresses the other side
tasks-timestampsMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-timestampsn/a: catalogue marks it n/a
tasks-ttl-override-reportMAY / MUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-ttl-override-reportn/a: catalogue marks it n/a
tasks-delete-after-ttlMAYboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-delete-after-ttln/a: catalogue marks it n/a
tasks-poll-intervalMAY / SHOULDbothtested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-poll-intervaln/a: catalogue marks it n/a
tasks-return-createtaskresultMUST / SHOULDboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-return-createtaskresultn/a: catalogue marks it n/a
tasks-result-terminal-finalMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-result-terminal-finaln/a: catalogue marks it n/a
tasks-result-blocksMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-result-blocksn/a: catalogue marks it n/a
tasks-result-exactMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-result-exactn/a: catalogue marks it n/a
tasks-related-task-all-messagesMUSTbothtested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-related-task-all-messagesn/a: catalogue marks it n/a
tasks-taskid-param-authoritativeMUST / SHOULD NOTbothtested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-taskid-param-authoritativen/a: catalogue marks it n/a
tasks-no-related-task-in-get-list-cancel-resultsSHOULD NOTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-no-related-task-in-get-list-cancel-resultsn/a: catalogue marks it n/a
tasks-result-response-related-taskMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-result-response-related-taskn/a: catalogue marks it n/a
tasks-list-paginateSHOULD / MUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-list-paginaten/a: catalogue marks it n/a
tasks-list-cursor-opaqueMUSTboth (requestor)n/a: catalogue marks it n/an/a: catalogue marks it n/a
tasks-get-implies-listMUSTboth (receiver)gap #209: a task started on a connection without an identity can be read by tasks/get and is never listedn/a: catalogue marks it n/a
tasks-cancel-terminal-rejectMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-cancel-terminal-rejectn/a: catalogue marks it n/a
tasks-cancel-transition-before-responseSHOULD / MUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-cancel-transition-before-responsen/a: catalogue marks it n/a
tasks-cancelled-stickyMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-cancelled-stickyn/a: catalogue marks it n/a
tasks-cancelled-may-deleteMAY / SHOULD NOTbothtested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-cancelled-may-deleten/a: catalogue marks it n/a
tasks-protocol-errorsMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-protocol-errorsn/a: catalogue marks it n/a
tasks-informative-errorsSHOULDboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-informative-errorsn/a: catalogue marks it n/a
tasks-failed-status-messageSHOULDboth (receiver)gap #209: a task failed by an isError tool result carries no statusMessagen/a: catalogue marks it n/a
tasks-result-same-errorMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-result-same-errorn/a: catalogue marks it n/a
tasks-bind-auth-contextMUSTboth (receiver)n/a: optional feature mcpx does not implementn/a: catalogue marks it n/a
tasks-document-unboundSHOULDboth (receiver)gap #209: nothing user-facing documents that an unowned task is reachable by anyone holding its idn/a: catalogue marks it n/a
tasks-unbound-secure-idsMUSTboth (receiver)tested: conformance.TestStatefulServer/2025-11-25/tasks/tasks-unbound-secure-idsn/a: catalogue marks it n/a
tasks-unidentified-no-listSHOULD NOTboth (receiver)tested: mcpserver.TestTasksPerEra/2025-11-25/tasks/another-connection-cannot-list-or-read-itn/a: catalogue marks it n/a
tasks-bound-reject-foreignMUSTboth (receiver)n/a: no contextn/a: catalogue marks it n/a
tasks-rate-limitSHOULDboth (receiver)gap #209: tasks/* has no rate limitn/a: catalogue marks it n/a
tasks-resource-limitsSHOULDboth (receiver)gap #209: no maximum ttl and no cap on concurrent tasks per requestorn/a: catalogue marks it n/a
tasks-auditSHOULDbothgap #209: task creation, completion and retrieval are not loggedn/a: catalogue marks it n/a

tools

requirementlevelsidemcpx as servermcpx as client
tools-human-in-loop-denySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-ui-expose-indicate-confirmSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-declare-capabilityn/a: addresses the other side
tools-list-changed-means-emit(definition)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-list-changed-means-emitn/a: addresses the other side
tools-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-list-changed-notifyn/a: addresses the other side
tools-annotations-untrustedMUSTclientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-annotations-hints-no-decisionsshould (lowercase, schema)clientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-inputschema-no-params-recommendedRECOMMENDEDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-inputschema-no-params-recommendedn/a: addresses the other side
tools-name-lengthSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-name-lengthn/a: addresses the other side
tools-name-case-sensitiveSHOULDbothtested: conformance.TestFeaturesServer/2025-11-25/tools/tools-name-case-sensitivetested: conformance.TestFeaturesClient/2025-11-25/tools/tools-name-case-sensitive
tools-name-charsetSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-name-charsetn/a: addresses the other side
tools-name-no-specialSHOULD NOTservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-name-no-specialn/a: addresses the other side
tools-name-unique-in-serverSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-name-unique-in-servern/a: addresses the other side
tools-result-iserror-for-tool-errorsSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-result-iserror-for-tool-errorsn/a: addresses the other side
tools-result-lookup-errors-protocolshould (lowercase)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-result-lookup-errors-protocoln/a: addresses the other side
tools-error-unknown-tool-protocol(classification)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-error-unknown-tool-protocoln/a: addresses the other side
tools-error-input-validation-execution(classification)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-error-input-validation-executionn/a: addresses the other side
tools-client-exec-errors-to-llmSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/tools/tools-client-exec-errors-to-llm
tools-client-protocol-errors-to-llm-mayMAYclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2025-11-25/tools/tools-client-protocol-errors-to-llm-may
tools-embedded-resource-mayMAYservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-embedded-resource-mayn/a: addresses the other side
tools-embedded-implies-resources-capSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-embedded-implies-resources-capn/a: addresses the other side
tools-resource-link-mayMAYservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-resource-link-mayn/a: addresses the other side
tools-resource-link-may-not-listed(note)bothtested: conformance.TestFeaturesServer/2025-11-25/tools/tools-resource-link-may-not-listedtested: conformance.TestFeaturesClient/2025-11-25/tools/tools-resource-link-may-not-listed
tools-structured-also-textSHOULDservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-structured-also-textn/a: addresses the other side
tools-structured-object-only(type, schema)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-structured-object-onlyn/a: addresses the other side
tools-output-schema-conformMUSTservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-output-schema-conformn/a: addresses the other side
tools-output-schema-client-validateSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-output-schema-object-root(type, schema)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-output-schema-object-rootn/a: addresses the other side
tools-display-name-precedence(rule)clientn/a: addresses the other sidegap #207: tool title and annotations.title are carried but nothing that displays a tool uses them
tools-server-sec-validate-inputsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-client-secSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement

tools / json-schema

requirementlevelsidemcpx as servermcpx as client
tools-inputschema-valid-not-nullMUSTservertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-inputschema-valid-not-nulln/a: addresses the other side
tools-inputschema-type-object-rootREQUIRED (schema)servertested: conformance.TestFeaturesServer/2025-11-25/tools/tools-inputschema-type-object-rootn/a: addresses the other side

transport-http-sse

requirementlevelsidemcpx as servermcpx as client
http-sse-server-backcompat-host-bothshould (lowercase)servern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-client-backcompat-probe-400-404-405should (lowercase)clientn/a: addresses the other sidetested: mcpclient.TestHTTPSSETransport/2025-11-25/transport-http-sse/backcompat-probe-refusal-is-recognised
pool.TestHTTPSSEFallback/2025-11-25/transport-http-sse/client-falls-back-to-get-endpoint-and-caches-it

transport-stdio

requirementlevelsidemcpx as servermcpx as client
transport-client-should-support-stdioSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-stdio/transport-client-should-support-stdio
transport-custom-preserve-jsonrpc-and-lifecycleMUSTbothn/a: mcpx serves MCP only over stdio and Streamable HTTP; its /v1 REST surface is not an MCP transportn/a: catalogue marks it n/a
transport-custom-document-patternsSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
stdio-no-embedded-newlinesMUST NOTbothtested: conformance.TestTransportServer/2025-11-25/transport-stdio/stdio-no-embedded-newlinestested: conformance.TestTransportClient/2025-11-25/transport-stdio/stdio-no-embedded-newlines
stdio-message-is-singleMUST (implied)bothtested: conformance.TestTransportServer/2025-11-25/transport-stdio/stdio-message-is-singletested: conformance.TestTransportClient/2025-11-25/transport-stdio/stdio-message-is-single
stdio-server-may-log-to-stderrMAYservertested: conformance.TestTransportServer/2025-11-25/transport-stdio/stdio-server-may-log-to-stderrn/a: addresses the other side
stdio-client-may-capture-stderrMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-stdio/stdio-client-may-capture-stderr
stdio-client-stderr-not-errorSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-stdio/stdio-client-stderr-not-error
stdio-server-stdout-only-mcpMUST NOTservertested: conformance.TestTransportServer/2025-11-25/transport-stdio/stdio-server-stdout-only-mcpn/a: addresses the other side
stdio-client-stdin-only-mcpMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-stdio/stdio-client-stdin-only-mcp

transport-streamable-http

requirementlevelsidemcpx as servermcpx as client
streamable-http-server-single-endpoint-post-getMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-single-endpoint-post-getn/a: addresses the other side
streamable-http-server-validate-originMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-validate-originn/a: addresses the other side
streamable-http-server-403-invalid-originMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-403-invalid-originn/a: addresses the other side
streamable-http-server-bind-localhostSHOULDservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-bind-localhostn/a: addresses the other side
streamable-http-server-authenticateSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-post-every-messageMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-post-every-message
streamable-http-client-accept-bothMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-accept-both
streamable-http-post-body-singleMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-post-body-single
streamable-http-server-202-for-non-requestMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-202-for-non-requestn/a: addresses the other side
streamable-http-server-error-status-if-rejectedMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-error-status-if-rejectedn/a: addresses the other side
streamable-http-server-json-or-sse-for-requestMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-json-or-sse-for-requestn/a: addresses the other side
streamable-http-client-supports-json-and-sseMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-supports-json-and-sse
streamable-http-sse-includes-responseSHOULDservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-sse-includes-responsen/a: addresses the other side
streamable-http-post-sse-may-carry-requestsMAYservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-post-sse-may-carry-requestsn/a: addresses the other side
streamable-http-server-may-terminate-on-session-expiryMAYservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-may-terminate-on-session-expiryn/a: addresses the other side
streamable-http-server-terminates-sse-after-responseSHOULDservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-terminates-sse-after-responsen/a: addresses the other side
streamable-http-sse-priming-eventSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-server-may-close-connection-keep-streamMAYservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-client-polls-after-closeSHOULDclientn/a: addresses the other sidetested: mcpclient.TestLegacyHTTPStreamResumption/2025-11-25/transport/client-resumes-closed-stream-with-last-event-id-after-retry
streamable-http-server-sends-retry-before-closeSHOULDservern/a: never closes earlyn/a: addresses the other side
streamable-http-client-respects-retryMUSTclientn/a: addresses the other sidetested: mcpclient.TestLegacyHTTPStreamResumption/2025-11-25/transport/client-resumes-closed-stream-with-last-event-id-after-retry
streamable-http-disconnect-not-cancelSHOULD NOTbothtested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-disconnect-not-canceln/a: catalogue marks it n/a
streamable-http-client-cancel-by-notificationSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-cancel-by-notification
streamable-http-server-may-resumeMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-may-getMAYclientn/a: addresses the other siden/a: optional feature mcpx does not implement
streamable-http-get-accept-sseMUSTclientn/a: addresses the other siden/a: no GET
streamable-http-get-sse-or-405MUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-get-sse-or-405n/a: addresses the other side
streamable-http-get-stream-unrelatedSHOULDservern/a: 405n/a: addresses the other side
streamable-http-get-no-responsesMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-get-close-any-timeMAYbothtested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-get-close-any-timetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-get-close-any-time
streamable-http-multiple-streamsMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-multiple-streams
streamable-http-no-broadcastMUST / MUST NOTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-no-broadcastn/a: addresses the other side
streamable-http-event-id-uniqueMUSTservern/a: no idsn/a: addresses the other side
streamable-http-event-id-encodes-streamSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-client-resume-with-last-event-idSHOULDclientn/a: addresses the other sidetested: mcpclient.TestLegacyHTTPStreamResumption/2025-11-25/transport/client-resumes-closed-stream-with-last-event-id-after-retry@2025-03-26,2025-06-18,2025-11-25
streamable-http-server-no-cross-stream-replayMUST NOTservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-server-may-assign-sessionMAYservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-may-assign-sessionn/a: addresses the other side
streamable-http-session-id-secureSHOULDservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-session-id-securen/a: addresses the other side
streamable-http-session-id-visible-asciiMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-session-id-visible-asciin/a: addresses the other side
streamable-http-client-handles-session-securelyMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-handles-session-securely
streamable-http-client-echoes-sessionMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-echoes-session
streamable-http-server-400-missing-sessionSHOULDservern/a: mcpx does not require one (stateless fallbackn/a: addresses the other side
streamable-http-server-404-terminated-sessionMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-404-terminated-sessionn/a: addresses the other side
streamable-http-client-reinit-on-404MUSTclientn/a: addresses the other sidetested: mcpclient.TestLegacySession404Recovery/2025-11-25/transport/client-reinitializes-on-session-404-and-retries-once@2025-03-26,2025-06-18,2025-11-25
streamable-http-client-delete-sessionSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-client-delete-session
streamable-http-server-may-405-deleteMAYservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-may-405-deleten/a: addresses the other side
streamable-http-protocol-version-header-is-negotiatedSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2025-11-25/transport-streamable-http/streamable-http-protocol-version-header-is-negotiated
streamable-http-server-assumes-2025-03-26SHOULDservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-assumes-2025-03-26n/a: addresses the other side
streamable-http-server-400-unsupported-version-headerMUSTservertested: conformance.TestTransportServer/2025-11-25/transport-streamable-http/streamable-http-server-400-unsupported-version-headern/a: addresses the other side

versioning

requirementlevelsidemcpx as servermcpx as client
lifecycle-client-sends-supported-versionMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/versioning/lifecycle-client-sends-supported-version
lifecycle-client-sends-latest-versionSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/versioning/lifecycle-client-sends-latest-version
lifecycle-client-may-support-olderMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/versioning/lifecycle-client-may-support-older
lifecycle-server-echoes-supported-versionMUSTservertested: conformance.TestLifecycleServer/2025-11-25/versioning/lifecycle-server-echoes-supported-versionn/a: addresses the other side
lifecycle-server-counteroffers-versionMUSTservertested: conformance.TestLifecycleServer/2025-11-25/versioning/lifecycle-server-counteroffers-versionn/a: addresses the other side
lifecycle-server-counteroffers-latestSHOULDservertested: conformance.TestLifecycleServer/2025-11-25/versioning/lifecycle-server-counteroffers-latestn/a: addresses the other side
lifecycle-client-disconnects-on-unsupported-versionSHOULD (prose) / MUST (schema)clientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/versioning/lifecycle-client-disconnects-on-unsupported-version
lifecycle-http-protocol-version-headerMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2025-11-25/versioning/lifecycle-http-protocol-version-header

2026-07-28

(security; not in area list)

requirementlevelsidemcpx as servermcpx as client
security-implementors-guidelinesSHOULDbothtested: conformance.TestMessagesServer/2026-07-28/security/security-implementors-guidelinesn/a: catalogue marks it n/a

authorization

requirementlevelsidemcpx as servermcpx as client
authorization-http-should-conform-stdio-should-notSHOULD / SHOULD NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authorization-custom-auth-mayMAYbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-optionalOPTIONALbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-http-should-conformSHOULDbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-stdio-use-environmentSHOULD NOTbothgap not implemented: #1975gap not implemented: #1950
authz-alt-transport-best-practiceMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-as-implements-oauth21MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-dcr-may-deprecatedMAYclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-bearer-headerMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-token-every-requestMUSTclientn/a: addresses the other sidegap not implemented: #1950
authz-no-token-in-queryMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-server-validates-tokenMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-validates-audienceMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-client-sends-only-own-as-tokensMUST NOTclientn/a: addresses the other sidegap not implemented: #1950
authz-accept-only-own-tokensMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-no-token-passthroughMUST NOTservergap not implemented: #1975n/a: addresses the other side
authz-error-status-codesMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-server-prm-rfc9728MUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-prm-lists-authorization-serversMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-as-metadata-rfc8414-or-oidcMUSTbothn/a: catalogue marks it n/an/a: optional feature mcpx does not implement
authz-prm-discovery-header-or-well-knownMUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-client-parses-www-authenticateMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-client-as-discovery-orderMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-client-validates-issuer-in-metadataMUST / MUST NOTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-client-per-as-registration-stateMUST / MUST NOTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-resource-indicatorMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-canonical-uri-guidanceSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-secure-token-storageMUSTbothn/a: catalogue marks it n/agap not implemented: #1950
authz-as-short-lived-and-rotateSHOULD / MUSTauth-servern/a: addresses the other siden/a: addresses the other side
authz-https-and-redirect-urisMUSTbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
authz-pkce-verify-s256MUSTclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-open-redirectionMUST / SHOULDclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-proxy-consent-per-clientMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-client-registration-requiredMUSTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-registration-prioritySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-cimdSHOULD / MUSTclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-preregistrationSHOULDclientn/a: addresses the other siden/a: no OAuth client
authz-dcr-application-typeMUST / SHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-credentials-bound-to-issuerMUST / MUST NOTclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-scope-in-challengeSHOULD / MUST / MUST NOTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-scope-selection-orderSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-insufficient-scope-403SHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-all-scopes-single-challengeSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-step-upSHOULD / MAYclientn/a: addresses the other siden/a: optional feature mcpx does not implement
authz-server-scope-hierarchyMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
authz-cimd-as-securityMUST / SHOULDauth-servern/a: addresses the other siden/a: addresses the other side
authz-iss-validationMUST / SHOULD / MUST NOTclient, auth-servern/a: addresses the other siden/a: optional feature mcpx does not implement
authz-refresh-tokensMUST / SHOULD / MAY / MUST NOT / SHOULD NOTclient, servern/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement
authz-follow-security-considerationsMUSTbothn/a: optional feature mcpx does not implementn/a: optional feature mcpx does not implement

caching

requirementlevelsidemcpx as servermcpx as client
caching-server-must-hintMUST*servertested: mcpserver.TestModernEnvelope/2026-07-28/caching/resources-read-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/server-discover-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/tools-list-has-ttlMs-and-cacheScope
n/a: addresses the other side
caching-input-required-no-hintsdescr.servertested: mcpserver.TestMRTRServerRules/2026-07-28/mrtr/requestState-is-bound-to-the-originating-requestn/a: addresses the other side
caching-key-method-paramsMUST NOTclientn/a: addresses the other siden/a: not applicable by construction – the pool catalogue cache is keyed by the configured server, not by method+params, and resources/read results are not cached by the pool (#1604)
caching-no-cache-mrtr-retriesMUST NOTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/caching/caching-no-cache-mrtr-retries
caching-ttl-zero-staleSHOULDclientn/a: addresses the other sidetested: mcpclient.TestListToolsWithHintsAggregatesPages@2026-07-28
pool.TestPoolCacheTTLZeroStale@2026-07-28
caching-ttl-positive-freshSHOULDclientn/a: addresses the other sidetested: pool.TestPoolCacheTTLPositiveExpires@2026-07-28
pool.TestPoolCacheTTLPositiveFresh@2026-07-28
caching-ttl-absent-zeroSHOULDclientn/a: addresses the other sidetested: mcpclient.TestListPromptsWithHintsAbsent@2026-07-28
pool.TestPoolCacheTTLAbsentStale@2026-07-28
caching-ttl-negative-zeroSHOULDclientn/a: addresses the other sidetested: pool.TestPoolCacheTTLNegativeStale@2026-07-28
caching-ttl-non-negativeMUSTservertested: conformance.TestStatefulServer/2026-07-28/caching/caching-ttl-non-negativen/a: addresses the other side
caching-stale-refetchSHOULDclientn/a: addresses the other sidetested: pool.TestPoolCacheConcurrentExpiredCollapse@2026-07-28
pool.TestPoolCacheTTLZeroStale@2026-07-28
caching-ttl-not-pollSHOULD NOT / MUSTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/caching/caching-ttl-not-poll
caching-may-refetch-early-serve-staleMAYclientn/a: addresses the other siden/a: a MAY mcpx does not take – refresh is synchronous on read (decision on #1601); stale is served only when a refetch of an expired entry fails (#1603)
caching-public-shareableMAYbothn/a: catalogue marks it n/an/a: informational – permits sharing a public result across users; imposes nothing
caching-private-no-cross-authMUST NOTboth (any cache)n/a: catalogue marks it n/atested: daemon.TestLoadCacheNeverSeedsPrivateEntry@2026-07-28
daemon.TestSaveCacheSkipsPrivatePools@2026-07-28
pool.TestPoolCacheStaleServedOnRefetchError@2026-07-28
caching-notification-invalidatesdescr. (lowercase should)clientn/a: addresses the other sidetested: daemon.TestListChangedInvalidatesPoolCache@2026-07-28
caching-page-refetchSHOULDclientn/a: addresses the other siden/a: not applicable by construction – the pool caches one aggregated catalogue per configured server, never per page; an expired catalogue is re-fetched whole (#1601, #1604)
caching-page-same-scopeMUSTservertested: mcpserver.TestModernEnvelope/2026-07-28/caching/every-page-has-the-same-cacheScopen/a: addresses the other side
caching-server-access-controlMUST* / MUST NOT*servern/a: until hints are emitted; mcpx has no per-primitive access control (see authorization arean/a: addresses the other side
cache-ttl-scope-requiredREQUIRED (schema)servertested: mcpserver.TestModernEnvelope/2026-07-28/caching/prompts-list-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/resources-list-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/resources-read-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/resources-templates-list-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/server-discover-has-ttlMs-and-cacheScope
mcpserver.TestModernEnvelope/2026-07-28/caching/tools-list-has-ttlMs-and-cacheScope
n/a: addresses the other side

cancellation

requirementlevelsidemcpx as servermcpx as client
cancellation-ref-client-issued-in-progressMUSTbothtested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/server-teardown-sends-cancelled-then-resulttested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-ref-client-issued-in-progress
cancellation-server-should-stop-free-no-responseSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-server-should-stop-free-no-responsen/a: addresses the other side
cancellation-server-may-ignoreMAYservertested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-server-may-ignoren/a: addresses the other side
cancellation-client-ignore-late-responseSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-client-ignore-late-response
cancellation-handle-racesMUSTbothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-handle-racestested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-handle-races
cancellation-log-reasonsSHOULDbothgap #77: a cancellation reason reaches Server.OnCancel, which nothing outside tests sets; it is never loggedtested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-log-reasons
cancellation-ui-indicateSHOULDbothn/a: no UIn/a: no UI
cancellation-ignore-invalidSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-ignore-invalidtested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-ignore-invalid
cancellation-processing-should-ceaseSHOULDboth (receiver)tested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-processing-should-ceasetested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-processing-should-cease
cancellation-client-should-sendSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-client-should-send
cancellation-server-must-send-cancelled-on-listen-teardownMUSTservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/server-teardown-sends-cancelled-then-resultn/a: addresses the other side
cancellation-server-only-for-listenMUST NOT / MUSTservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-cancelled-only-for-listen
mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/server-teardown-sends-cancelled-then-result
n/a: addresses the other side
cancellation-http-disconnect-is-cancelMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-close-is-canceln/a: addresses the other side
cancellation-http-no-messages-after-closeMUST NOT / SHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-close-is-cancel
conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-silent-after-cancel
n/a: addresses the other side
cancellation-stdio-client-must-sendMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-stdio-client-must-send
cancellation-timeouts-establishSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-timeouts-establishtested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-timeouts-establish
cancellation-timeout-then-cancelSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-timeout-then-canceltested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-timeout-then-cancel
cancellation-timeouts-per-request-configurableSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-timeouts-per-request-configurabletested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-timeouts-per-request-configurable
cancellation-max-timeout-despite-progressSHOULD (+MAY)bothtested: conformance.TestUtilitiesServer/2026-07-28/cancellation/cancellation-max-timeout-despite-progresstested: conformance.TestUtilitiesClient/2026-07-28/cancellation/cancellation-max-timeout-despite-progress

completion

requirementlevelsidemcpx as servermcpx as client
completion-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2026-07-28/completion/completion-declare-capabilityn/a: addresses the other side
completion-max-100descr. + schemaservertested: conformance.TestUtilitiesServer/2026-07-28/completion/completion-max-100n/a: addresses the other side
completion-server-shouldSHOULDservertested: daemon.TestHandleCompleteRateLimitHotChangeAndZeroDisables@all
daemon.TestHandleCompleteRateLimitRefusesBeforePool@all
daemon.TestHandleCompleteServerWindowBindsAcrossCallers@all
daemon.TestLocalCompletionCapAfterRanking@all
daemon.TestLocalCompletionTiersPrompts@all
daemon.TestLocalCompletionTiersResources@all
n/a: addresses the other side
completion-client-shouldSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/completion/completion-client-should
completion-securityMUSTbothgap #1968: partial: completion inputs are validated and completion/complete is rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) since #2387, but there is no per-caller access control for sensitive suggestions and no completion-specific information-disclosure control; waiting on principals #1968 / permissive-auth #1972n/a: catalogue marks it n/a
completion-errorsSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/completion/completion-errorsn/a: addresses the other side
completion-context-argumentsshould (lowercase)clientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/completion/completion-context-arguments

discover

requirementlevelsidemcpx as servermcpx as client
discover-client-may-callMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/discover/discover-client-may-call
discover-supportedversionsREQUIRED (schema)servertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-supportedversionsn/a: addresses the other side
discover-client-choose-from-listshould (lowercase)clientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/discover/discover-client-choose-from-list
discover-capabilitiesREQUIRED (schema)servertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-capabilitiesn/a: addresses the other side
discover-serverinfo-in-metaSHOULDservertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-serverinfo-in-metan/a: addresses the other side
discover-serverinfo-not-for-behaviorSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/discover/discover-serverinfo-not-for-behavior
discover-instructions-optionalMAYservertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-instructions-optionaln/a: addresses the other side

discover / caching

requirementlevelsidemcpx as servermcpx as client
discover-cacheableREQUIRED (schema)servertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-cacheablen/a: addresses the other side

discover / meta

requirementlevelsidemcpx as servermcpx as client
discover-resulttypeMUSTservertested: conformance.TestLifecycleServer/2026-07-28/discover/discover-resulttypen/a: addresses the other side
discover-request-meta-requiredMUSTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/discover/discover-request-meta-required

elicitation

requirementlevelsidemcpx as servermcpx as client
elicitation-server-no-sensitive-info-in-form-modeMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-no-sensitive-info-in-form-moden/a: addresses the other side
elicitation-server-sensitive-via-url-modeMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-sensitive-via-url-moden/a: addresses the other side
elicitation-client-show-requesting-serverMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-show-requesting-server
elicitation-client-form-review-modify-responsesMUSTclientn/a: addresses the other sidetested: cli.TestRenderAnswerReviewMasksSensitiveFields@2025-11-25,2026-07-28
cli.TestRenderQuestionFormShowsSchemaDefaults@2025-11-25,2026-07-28
elicitation-client-decline-cancel-optionsMUSTclientn/a: addresses the other sidetested: elicit.TestAnExpiredQuestionBecomesCancelNotDecline@2025-06-18,2025-11-25,2026-07-28
elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-url-show-domain-and-consentMUSTclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-declare-capability-per-request-metaMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-declare-capability-per-request-meta
elicitation-empty-capability-means-form-only(definition)bothtested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-empty-capability-means-form-onlytested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-empty-capability-means-form-only
elicitation-client-support-at-least-one-modeMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-support-at-least-one-mode
elicitation-server-no-undeclared-modesMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-no-undeclared-modesn/a: addresses the other side
elicitation-server-may-request-during-client-requestMAYservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-may-request-during-client-requestn/a: addresses the other side
elicitation-request-must-include-mode-and-messageMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-request-must-include-mode-and-messagen/a: addresses the other side
elicitation-server-may-omit-mode-for-formMAYservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-may-omit-mode-for-formn/a: addresses the other side
elicitation-client-missing-mode-is-formMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-missing-mode-is-form
elicitation-form-request-shapeMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-form-request-shapen/a: addresses the other side
elicitation-form-schema-flat-primitivesMUST (schema-structural)servertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-form-schema-flat-primitivesn/a: addresses the other side
elicitation-client-prepopulate-defaultsSHOULDclientn/a: addresses the other sidetested: mcpclient.TestModernElicitationAppliesSchemaDefaults/2026-07-28/elicitation/client-applies-schema-defaults
elicitation-url-request-shapeMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-request-shapen/a: addresses the other side
elicitation-url-must-be-validMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-must-be-validn/a: addresses the other side
elicitation-client-manual-retry-cancel-controlsSHOULDclientn/a: addresses the other sidetested: e2e.TestElicitationsCanBeListedAndAnsweredFromTheCommandLine@2025-11-25,2026-07-28
elicitation-no-32042-in-2026MUST NOTbothtested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-no-32042-in-2026tested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-no-32042-in-2026
elicitation-server-stored-state-bound-to-userMUSTservergap #257: mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sendsn/a: addresses the other side
elicitation-state-storage-protectedMUSTservertested: elicit.TestOpenAtCreatesOwnerOnlyStore@2025-11-25,2026-07-28
elicit.TestOpenAtMigratesLegacyStore@2025-11-25,2026-07-28
n/a: addresses the other side
elicitation-remote-user-id-from-authorizationMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
elicitation-server-must-not-self-authorize-via-urlMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-must-not-self-authorize-via-urln/a: addresses the other side
elicitation-third-party-creds-not-via-clientMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-third-party-creds-not-via-clientn/a: addresses the other side
elicitation-no-token-passthroughMUST NOTservern/a: optional feature mcpx does not implementn/a: addresses the other side
elicitation-user-authorizes-server-directlyMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-user-authorizes-server-directlyn/a: addresses the other side
elicitation-server-not-transmit-url-creds-to-clientMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-not-transmit-url-creds-to-clientn/a: addresses the other side
elicitation-server-handle-decline-cancel-failureSHOULD NOT / MUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-handle-decline-cancel-failuren/a: addresses the other side
elicitation-server-bind-to-client-and-userMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-server-bind-to-client-and-usern/a: addresses the other side
elicitation-client-user-approval-controlsSHOULDclientn/a: addresses the other sidetested: elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28
elicitation-client-allow-decline-any-timeSHOULDclientn/a: addresses the other sidetested: elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28
elicitation-client-clear-what-and-whySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-clear-what-and-why
elicitation-client-validate-responsesSHOULDclientn/a: addresses the other sidetested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejected
elicitation-server-validate-received-dataSHOULDservertested: mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejectedn/a: addresses the other side
elicitation-url-no-sensitive-user-infoMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-no-sensitive-user-infon/a: addresses the other side
elicitation-url-not-pre-authenticatedMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-not-pre-authenticatedn/a: addresses the other side
elicitation-no-clickable-urls-in-formSHOULD NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-no-clickable-urls-in-formn/a: addresses the other side
elicitation-url-https-outside-devSHOULDservertested: conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-https-outside-devn/a: addresses the other side
elicitation-client-handle-urls-carefullyMUSTclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-no-url-prefetchMUST NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-no-url-prefetch
elicitation-client-no-open-without-consentMUST NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-no-open-without-consent
elicitation-client-show-full-urlMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-show-full-url
elicitation-client-open-url-securelyMUSTclientn/a: addresses the other sidetested: elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28
elicitation-client-highlight-domainSHOULDclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLShowsRegistrableDomain@2025-11-25,2026-07-28
elicitation-client-warn-punycodeSHOULDclientn/a: addresses the other sidetested: cli.TestRenderQuestionURLWarnsOnPunycodeHost@2025-11-25,2026-07-28
elicitation-client-no-clickable-except-url-fieldSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-no-clickable-except-url-field
elicitation-server-no-trust-client-user-idMUST NOTservergap #257: mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sendsn/a: addresses the other side
elicitation-server-follow-best-practicesSHOULDservergap #255: requestState signing is sound and question storage is owner-only (0700/0600, #1676); there is no user identityn/a: addresses the other side
elicitation-url-verify-opening-userMUSTservern/a: mcpx issues no URL; the upstream that does sees mcpx’s identity, not the end user’sn/a: addresses the other side
elicitation-url-same-user-start-and-completeMUSTservern/a: see Implied: shared pooled upstreamn/a: addresses the other side
elicitation-url-identity-resilient-to-url-tamperingMUSTservern/a: catalogue marks it n/an/a: addresses the other side

errors

requirementlevelsidemcpx as servermcpx as client
messages-error-response-same-id-unless-unreadableMUSTbothtested: conformance.TestMessagesServer/2026-07-28/errors/messages-error-response-same-id-unless-unreadabletested: conformance.TestMessagesClient/2026-07-28/errors/messages-error-response-same-id-unless-unreadable
errors-error-has-code-and-messageMUSTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-error-has-code-and-messagetested: conformance.TestMessagesClient/2026-07-28/errors/errors-error-has-code-and-message
errors-codes-integersMUSTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-codes-integerstested: conformance.TestMessagesClient/2026-07-28/errors/errors-codes-integers
errors-message-concise-single-sentenceSHOULDbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-message-concise-single-sentencen/a: sends no errors except to server requests
errors-data-mayMAYbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-data-mayn/a: catalogue marks it n/a
errors-standard-jsonrpc-codes(definitional)bothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-standard-jsonrpc-codesn/a: catalogue marks it n/a
errors-legacy-subrange-no-new-codesMUST NOT / SHOULD NOTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-legacy-subrange-no-new-codestested: conformance.TestMessagesClient/2026-07-28/errors/errors-legacy-subrange-no-new-codes
errors-receivers-no-meaning-for-legacy-subrangeMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-receivers-no-meaning-for-legacy-subrangetested: conformance.TestMessagesClient/2026-07-28/errors/errors-receivers-no-meaning-for-legacy-subrange
errors-reserved-subrange-only-spec-codesMUST NOT / MUSTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-reserved-subrange-only-spec-codesn/a: catalogue marks it n/a
errors-do-not-emit-32002-and-32042MUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-do-not-emit-32002-and-32042n/a: catalogue marks it n/a
errors-client-accept-32002-from-legacySHOULDclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/errors/errors-client-accept-32002-from-legacy
errors-local-errors-not-mistaken-for-peershould (lowercase)bothgap #206: an upstream server’s failure reaches the client as mcpx’s own isError text or -32603, indistinguishable from mcpx failingn/a: catalogue marks it n/a
errors-new-codes-outside-jsonrpc-rangeSHOULDbothtested: conformance.TestMessagesServer/2026-07-28/errors/errors-new-codes-outside-jsonrpc-rangen/a: catalogue marks it n/a
errors-method-not-found-for-unadvertised-capability(schema guidance)servergap #257: mcpx answers methods whatever was negotiated (accept liberally, revisions.go); the schema guidance asks for -32601n/a: addresses the other side
errors-missing-required-client-capabilityMUSTservern/a: in practice: mcpx never requires a client capability; questions a client cannot answer go to the brokern/a: addresses the other side
versioning-unsupported-version-http-400MUSTservertested: conformance.TestMessagesServer/2026-07-28/errors/versioning-unsupported-version-http-400n/a: addresses the other side
errors-header-mismatch-http-400MUSTservertested: conformance.TestMessagesServer/2026-07-28/errors/errors-header-mismatch-http-400n/a: addresses the other side

errors (roots/sampling/elicitation)

requirementlevelsidemcpx as servermcpx as client
mrtr-missing-capability-error-32021MUSTservergap #257: through the gateway (mcpx_call) a call that needs input from a client that declared none goes to the broker instead of failing with -32021; the specification contradicts itself here (docs/spec/revision-conflicts.md). In pass-through mode the client’s declaration is relayed and the upstream’s -32021 returned (mcpserver.TestAPassThroughUpstreamsMissingCapabilityIsRelayed)n/a: addresses the other side

json-schema

requirementlevelsidemcpx as servermcpx as client
json-schema-default-2020-12(definitional)bothtested: conformance.TestMessagesServer/2026-07-28/json-schema/json-schema-default-2020-12n/a: catalogue marks it n/a
json-schema-may-declare-dialectMAYbothtested: conformance.TestMessagesServer/2026-07-28/json-schema/json-schema-may-declare-dialecttested: conformance.TestMessagesClient/2026-07-28/json-schema/json-schema-may-declare-dialect
json-schema-support-2020-12MUSTbothtested: mcpclient.TestCallToolRejectsInvalidArgumentsBeforeTheTransport@alln/a: passes through
json-schema-validate-per-dialect-and-error-on-unsupportedMUSTbothtested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
tested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
json-schema-document-dialectsSHOULDbothtested: schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
tested: schemaval.TestDeclaredDialectsCompile@all
schemaval.TestUnsupportedDialect@all
json-schema-recommend-2020-12RECOMMENDEDbothtested: conformance.TestMessagesServer/2026-07-28/json-schema/json-schema-recommend-2020-12n/a: catalogue marks it n/a
json-schema-schemas-validMUSTbothtested: conformance.TestMessagesServer/2026-07-28/json-schema/json-schema-schemas-validn/a: catalogue marks it n/a
json-schema-no-network-refMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/json-schema/json-schema-no-network-reftested: conformance.TestMessagesClient/2026-07-28/json-schema/json-schema-no-network-ref
json-schema-remote-ref-opt-in-off-by-defaultMAY / MUST / SHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
json-schema-reject-unresolved-refSHOULDbothtested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestRemoteRefMakesNoRequests@all
schemaval.TestUnresolvedInternalRef@all
tested: mcpclient.TestListToolsExcludesUncompilableSchemasWithWarning@all
schemaval.TestRemoteRefMakesNoRequests@all
schemaval.TestUnresolvedInternalRef@all
json-schema-bound-composition-costSHOULDbothtested: schemaval.TestSchemaTooLarge@alltested: schemaval.TestSchemaTooLarge@all
tools-schema-default-2020-12(definition)bothtested: conformance.TestMessagesServer/2026-07-28/json-schema/tools-schema-default-2020-12tested: conformance.TestMessagesClient/2026-07-28/json-schema/tools-schema-default-2020-12

lifecycle

requirementlevelsidemcpx as servermcpx as client
lifecycle-capability-sets-open(definitional)bothtested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-capability-sets-opentested: conformance.TestLifecycleClient/2026-07-28/lifecycle/lifecycle-capability-sets-open
lifecycle-implementation-name-version-requiredREQUIRED (schema field)bothtested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-implementation-name-version-requiredtested: conformance.TestLifecycleClient/2026-07-28/lifecycle/lifecycle-implementation-name-version-required
lifecycle-include-context-only-if-declaredSHOULDservertested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-include-context-only-if-declaredn/a: addresses the other side
lifecycle-stateless-no-reliance-on-prior-requestsMUST NOTservertested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-stateless-no-reliance-on-prior-requestsn/a: addresses the other side
lifecycle-stateless-multiple-conversationsSHOULDservertested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-stateless-multiple-conversationsn/a: addresses the other side
lifecycle-stateless-no-connection-reuse-requiredSHOULD NOTservertested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-stateless-no-connection-reuse-requiredn/a: addresses the other side
lifecycle-stateless-client-process-lifetimeSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/lifecycle/lifecycle-stateless-client-process-lifetime
lifecycle-stateless-explicit-handlesMUSTbothtested: conformance.TestLifecycleServer/2026-07-28/lifecycle/lifecycle-stateless-explicit-handlesn/a: catalogue marks it n/a

logging

requirementlevelsidemcpx as servermcpx as client
logging-declare-capabilityMUSTservertested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-declare-capabilityn/a: addresses the other side
logging-no-message-without-loglevelMUST NOTservertested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-no-message-without-logleveln/a: addresses the other side
logging-request-scoped-onlyMUST NOTservertested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-request-scoped-onlyn/a: addresses the other side
logging-may-send-before-responseMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
logging-invalid-loglevel-reject-requestSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-invalid-loglevel-reject-requestn/a: addresses the other side
logging-server-hygieneSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-server-hygienen/a: addresses the other side
logging-no-secrets-piiMUST NOTboth (emitter)tested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-no-secrets-piitested: conformance.TestUtilitiesClient/2026-07-28/logging/logging-no-secrets-pii
logging-impl-validate-controlSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-impl-validate-controltested: conformance.TestUtilitiesClient/2026-07-28/logging/logging-impl-validate-control
logging-deprecatedSHOULD NOT / SHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/logging/logging-deprecatedn/a: catalogue marks it n/a

messages

requirementlevelsidemcpx as servermcpx as client
messages-all-messages-follow-jsonrpc-2MUSTbothtested: conformance.TestJSONRPCFramesEveryRevision/2026-07-28/messages/all-messages-follow-jsonrpc-2-server-http
conformance.TestJSONRPCFramesEveryRevision/2026-07-28/messages/all-messages-follow-jsonrpc-2-server-stdio
conformance.TestMessagesServer/2026-07-28/messages/messages-all-messages-follow-jsonrpc-2
tested: conformance.TestJSONRPCFramesEveryRevision/2026-07-28/messages/all-messages-follow-jsonrpc-2-client
messages-other-components-mayMAYbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-other-components-maytested: conformance.TestMessagesClient/2026-07-28/messages/messages-other-components-may
messages-request-id-string-or-integerMUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-request-id-string-or-integertested: conformance.TestMessagesClient/2026-07-28/messages/messages-request-id-string-or-integer
messages-request-id-not-nullMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-request-id-not-nulltested: conformance.TestMessagesClient/2026-07-28/messages/messages-request-id-not-null
messages-request-id-unique-among-in-flightMUST NOTbothn/a: modern server sends no requeststested: conformance.TestMessagesClient/2026-07-28/messages/messages-request-id-unique-among-in-flight
messages-requests-client-to-server-only(definitional)bothn/a: on modern path; on legacy path mcpx still sends elicitation/sampling requests (allowed theretested: conformance.TestMessagesClient/2026-07-28/messages/messages-requests-client-to-server-only
messages-servers-must-not-initiate-requestsMUST NOTservertested: conformance.TestMessagesServer/2026-07-28/messages/messages-servers-must-not-initiate-requestsn/a: addresses the other side
messages-result-response-same-idMUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-result-response-same-idtested: conformance.TestMessagesClient/2026-07-28/messages/messages-result-response-same-id
messages-result-response-has-resultMUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-result-response-has-resulttested: conformance.TestMessagesClient/2026-07-28/messages/messages-result-response-has-result
messages-result-any-objectMAYbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-result-any-objecttested: conformance.TestMessagesClient/2026-07-28/messages/messages-result-any-object
messages-notification-no-responseMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-notification-no-responsetested: conformance.TestMessagesClient/2026-07-28/messages/messages-notification-no-response
messages-notification-no-idMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-notification-no-idtested: conformance.TestMessagesClient/2026-07-28/messages/messages-notification-no-id
messages-patterns-notifications-scoped-to-requestMAYservertested: conformance.TestMessagesServer/2026-07-28/messages/messages-patterns-notifications-scoped-to-requestn/a: addresses the other side
messages-resulttype-requiredMUSTservertested: conformance.TestMessagesServer/2026-07-28/messages/messages-resulttype-requiredn/a: addresses the other side
messages-resulttype-extension-valuesMAY / MUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/messages-resulttype-extension-valuestested: conformance.TestMessagesClient/2026-07-28/messages/messages-resulttype-extension-values
messages-resulttype-unknown-invalidMUSTclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/messages/messages-resulttype-unknown-invalid
messages-resulttype-absent-means-completeMUSTclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/messages/messages-resulttype-absent-means-complete
messages-resulttype-server-must-includeMUSTservertested: conformance.TestMessagesServer/2026-07-28/messages/messages-resulttype-server-must-includen/a: addresses the other side
transport-jsonrpc-utf8MUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/transport-jsonrpc-utf8tested: conformance.TestMessagesClient/2026-07-28/messages/transport-jsonrpc-utf8
transport-binding-message-directionsMUSTbothtested: conformance.TestMessagesServer/2026-07-28/messages/transport-binding-message-directionstested: conformance.TestMessagesClient/2026-07-28/messages/transport-binding-message-directions
transport-binding-may-mirror-metadataMAYbothtested: conformance.TestMessagesServer/2026-07-28/messages/transport-binding-may-mirror-metadatatested: conformance.TestMessagesClient/2026-07-28/messages/transport-binding-may-mirror-metadata

meta

requirementlevelsidemcpx as servermcpx as client
meta-no-assumptions-on-reserved-keysMUST NOTbothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-no-assumptions-on-reserved-keystested: conformance.TestMessagesClient/2026-07-28/meta/meta-no-assumptions-on-reserved-keys
meta-prefix-label-formatMUSTbothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-prefix-label-formattested: conformance.TestMessagesClient/2026-07-28/meta/meta-prefix-label-format
meta-reverse-dns-shouldSHOULDbothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-reverse-dns-shouldtested: conformance.TestMessagesClient/2026-07-28/meta/meta-reverse-dns-should
meta-reserved-prefix-second-label(reservation)bothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-reserved-prefix-second-labeltested: conformance.TestMessagesClient/2026-07-28/meta/meta-reserved-prefix-second-label
meta-name-formatMUST / MAYbothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-name-formattested: conformance.TestMessagesClient/2026-07-28/meta/meta-name-format
meta-schema-may-reserve-names(reservation)bothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-schema-may-reserve-namestested: conformance.TestMessagesClient/2026-07-28/meta/meta-schema-may-reserve-names
meta-progress-token-receiver-not-obligated(definitional)bothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-progress-token-receiver-not-obligatedtested: conformance.TestMessagesClient/2026-07-28/meta/meta-progress-token-receiver-not-obligated
meta-request-params-meta-requiredMUST / REQUIREDclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/meta/meta-request-params-meta-required
meta-server-rejects-missing-required-fieldMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side
meta-client-should-send-clientinfoSHOULDclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/meta/meta-client-should-send-clientinfo
meta-server-no-undeclared-client-capabilitiesMUST NOTservertested: conformance.TestMessagesServer/2026-07-28/meta/meta-server-no-undeclared-client-capabilitiesn/a: addresses the other side
meta-server-no-infer-caps-from-priorMUST NOTservertested: conformance.TestMessagesServer/2026-07-28/meta/meta-server-no-infer-caps-from-priorn/a: addresses the other side
meta-server-should-send-serverinfoSHOULDservertested: conformance.TestMessagesServer/2026-07-28/meta/meta-server-should-send-serverinfon/a: addresses the other side
meta-info-not-for-behavior-or-securitySHOULD NOTbothtested: conformance.TestMessagesServer/2026-07-28/meta/meta-info-not-for-behavior-or-securitytested: conformance.TestMessagesClient/2026-07-28/meta/meta-info-not-for-behavior-or-security
meta-subscription-id-on-listen-notificationsMUSTservertested: conformance.TestMessagesServer/2026-07-28/meta/meta-subscription-id-on-listen-notificationsn/a: addresses the other side
meta-otel-keys-w3c-formatMUSTbothn/a: does not emit); passes through? not verifiedn/a: catalogue marks it n/a
meta-icons-client-mime-supportMUST / SHOULDclientn/a: addresses the other siden/a: renders no icons
meta-icons-consumer-securityMUSTclientn/a: addresses the other siden/a: never fetches icons; passes metadata through
meta-icons-reject-unsafe-schemesMUSTclientn/a: addresses the other siden/a: does not dereference). Downstream host must do it
meta-icons-may-limit-sizeMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
meta-icons-same-originSHOULDclientn/a: addresses the other siden/a: catalogue marks it n/a
meta-request-required-fieldsMUSTclientn/a: addresses the other sidetested: conformance.TestMessagesClient/2026-07-28/meta/meta-request-required-fields

mrtr

requirementlevelsidemcpx as servermcpx as client
mrtr-server-requests-via-mrtrMUSTservertested: mcpserver.TestElicitationPerEra/2026-07-28/elicitation/inputRequests-carry-no-elicitationIdn/a: addresses the other side
mrtr-requeststate-opaque-clientMUST NOTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/mrtr/mrtr-requeststate-opaque-client
mrtr-supported-requests-onlyMAY / MUST NOTservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-supported-requests-onlyn/a: addresses the other side
mrtr-inputrequests-keys-valuesMUSTservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-inputrequests-keys-valuesn/a: addresses the other side
mrtr-requeststate-attacker-controlledMUSTservertested: mcpserver.TestMRTRServerRules/2026-07-28/mrtr/tampered-requestState-is-rejectedn/a: addresses the other side
mrtr-requeststate-replay-fieldsSHOULDservertested: mcpserver.TestMRTRServerRules/2026-07-28/mrtr/requestState-is-bound-to-the-originating-requestn/a: addresses the other side
mrtr-single-use-enforcedMUSTservern/a: binds only a server whose requestState must be consumed once; mcpx’s is re-minted every round and replaying it re-enters the same running calln/a: addresses the other side
mrtr-at-least-one-fieldMUSTservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-at-least-one-fieldn/a: addresses the other side
mrtr-only-declared-capabilitiesMUST NOTservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-only-declared-capabilitiesn/a: addresses the other side
mrtr-no-assume-retryMUST NOT / MAYservertested: mcpserver.TestMRTRServerRules/2026-07-28/mrtr/missing-input-response-asks-againn/a: addresses the other side
mrtr-client-fulfil-before-retryMUST / MAYclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/mrtr/mrtr-client-fulfil-before-retry
mrtr-client-echo-stateMUST / MUST NOTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/mrtr/mrtr-client-echo-state
mrtr-new-id-on-retryMUSTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/mrtr/mrtr-new-id-on-retry
mrtr-no-cross-request-useMUST NOTclientn/a: addresses the other sidetested: conformance.TestStatefulClient/2026-07-28/mrtr/mrtr-no-cross-request-use
mrtr-server-validate-responsesSHOULDservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-server-validate-responsesn/a: addresses the other side
mrtr-input-request-type-restrictedMUSTservertested: conformance.TestStatefulServer/2026-07-28/mrtr/mrtr-input-request-type-restrictedn/a: addresses the other side
mrtr-server-reask-missing-rather-than-errorSHOULDservertested: mcpserver.TestMRTRServerRules/2026-07-28/mrtr/missing-input-response-asks-againn/a: addresses the other side

pagination

requirementlevelsidemcpx as servermcpx as client
pagination-no-fixed-page-sizeMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/pagination/pagination-no-fixed-page-size
pagination-stable-cursorsSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/pagination/pagination-stable-cursorsn/a: addresses the other side
pagination-invalid-cursor-gracefulSHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/pagination/pagination-invalid-cursor-gracefuln/a: addresses the other side
pagination-missing-nextcursor-endSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/pagination/pagination-missing-nextcursor-end
pagination-support-both-flowsSHOULDclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/pagination/pagination-support-both-flows
pagination-cursor-opaqueMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/pagination/pagination-cursor-opaque
pagination-empty-cursor-not-endMUST NOTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/pagination/pagination-empty-cursor-not-end
pagination-invalid-cursor-32602SHOULDservertested: conformance.TestUtilitiesServer/2026-07-28/pagination/pagination-invalid-cursor-32602n/a: addresses the other side

progress

requirementlevelsidemcpx as servermcpx as client
progress-token-string-or-intMUSTboth (requester)n/a: as a server mcpx never requests progress; a host’s token is relayed back verbatimtested: conformance.TestUtilitiesClient/2026-07-28/progress/progress-token-string-or-int
progress-token-unique-clientMUSTclientn/a: addresses the other sidetested: conformance.TestUtilitiesClient/2026-07-28/progress/progress-token-unique-client
progress-server-may-sendMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
progress-must-increaseMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-must-increasen/a: catalogue marks it n/a
progress-message-human-readableSHOULDboth (notifier)tested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-message-human-readablen/a: catalogue marks it n/a
progress-only-active-tokensMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-only-active-tokensn/a: catalogue marks it n/a
progress-server-may-skipMAYservertested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-server-may-skipn/a: addresses the other side
progress-track-tokensSHOULDbothn/a: catalogue marks it n/atested: conformance.TestUtilitiesClient/2026-07-28/progress/progress-track-tokens
progress-rate-limitSHOULDbothtested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-rate-limittested: conformance.TestUtilitiesClient/2026-07-28/progress/progress-rate-limit
progress-stop-after-completionMUSTboth (notifier)tested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-stop-after-completionn/a: catalogue marks it n/a
progress-http-notifications-relate-to-requestMUSTservertested: conformance.TestUtilitiesServer/2026-07-28/progress/progress-http-notifications-relate-to-requestn/a: addresses the other side

prompts

requirementlevelsidemcpx as servermcpx as client
prompts-declare-capability-discoverMUSTservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-declare-capability-discovern/a: addresses the other side
prompts-list-must-respond-when-declaredMUSTservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-list-must-respond-when-declaredn/a: addresses the other side
prompts-list-not-per-connectionMUST NOTservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-list-not-per-connectionn/a: addresses the other side
prompts-list-may-vary-by-authMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
prompts-list-changed-notify-to-listenersSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-list-changed-notify-to-listenersn/a: addresses the other side
prompts-image-base64-mimeMUSTservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-image-base64-mimen/a: addresses the other side
prompts-audio-base64-mimeMUST (not bolded)servertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-audio-base64-mimen/a: addresses the other side
prompts-resource-link-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-resource-link-mayn/a: addresses the other side
prompts-embedded-must-includeMUSTservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-embedded-must-includen/a: addresses the other side
prompts-validate-argsSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-validate-argsn/a: addresses the other side
prompts-respect-capability-negotiationSHOULDbothtested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-respect-capability-negotiationtested: conformance.TestFeaturesClient/2026-07-28/prompts/prompts-respect-capability-negotiation
prompts-validate-ioMUSTbothtested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-validate-io; mcpclient.TestValidatePromptResultcovered

prompts / errors

requirementlevelsidemcpx as servermcpx as client
prompts-errors-32602-32603SHOULDservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-errors-32602-32603n/a: addresses the other side

prompts / mrtr

requirementlevelsidemcpx as servermcpx as client
prompts-get-input-required-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/prompts/prompts-get-input-required-mayn/a: addresses the other side

prompts / pagination

requirementlevelsidemcpx as servermcpx as client
prompts-client-paginateSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/prompts/prompts-client-paginate

resources

requirementlevelsidemcpx as servermcpx as client
resources-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-declare-capabilityn/a: addresses the other side
resources-subscribe-cap-meaning-2026(definition)servertested: e2e.TestResourceSubscriptionsReachUpstream/2026-07-28/subscriptions/listen-delivers-upstream-updates-and-acks-only-what-it-cann/a: addresses the other side
resources-cap-features-independentMAYservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-cap-features-independentn/a: addresses the other side
resources-list-must-respond-when-declaredMUSTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-list-must-respond-when-declaredn/a: addresses the other side
resources-list-not-per-connectionMUST NOTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-list-not-per-connectionn/a: addresses the other side
resources-list-may-vary-by-authMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-list-changed-notifySHOULDservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-list-changed-notifyn/a: addresses the other side
resources-read-multiple-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-read-multiple-mayn/a: addresses the other side
resources-read-https-direct-maymay (lowercase)clientn/a: addresses the other siden/a: optional feature mcpx does not implement
resources-updated-may-be-subresource(note, schema)clientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/resources/resources-updated-may-be-subresource
resources-https-only-if-client-fetchableSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-https-only-if-client-fetchablen/a: addresses the other side
resources-https-else-other-schemeSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-https-else-other-schemen/a: addresses the other side
resources-file-xdg-mime-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-file-xdg-mime-mayn/a: addresses the other side
resources-custom-scheme-rfc3986MUSTservertested: cli.TestMCPXResourceURIs/2026-07-28/resources/resources-custom-scheme-rfc3986n/a: addresses the other side
resources-no-empty-contents-for-missingMUST NOTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-no-empty-contents-for-missingn/a: addresses the other side
resources-sec-validate-urisMUSTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-sec-validate-urisn/a: addresses the other side
resources-sec-access-controlSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-binary-encodedMUSTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-sec-binary-encodedn/a: addresses the other side
resources-sec-permissionsSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
resources-sec-file-traversalMUSTservern/a: mcpx serves no file:// resources of its own; artifacts are addressed by opaque idsn/a: addresses the other side
resources-annotations-defs(definition)bothtested: conformance.TestFeaturesServer/2026-07-28/resources/resources-annotations-defsgap #207: mcpclient.Resource has no annotations field; they are dropped on parse

resources / errors

requirementlevelsidemcpx as servermcpx as client
resources-internal-32603SHOULDservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-internal-32603n/a: addresses the other side
resources-not-found-32602MUSTservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-not-found-32602n/a: addresses the other side
resources-client-accept-32002SHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/resources/resources-client-accept-32002

resources / mrtr

requirementlevelsidemcpx as servermcpx as client
resources-read-input-required-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/resources/resources-read-input-required-mayn/a: addresses the other side

resources / subscriptions

requirementlevelsidemcpx as servermcpx as client
resources-subscribe-via-listen(mechanism)clientn/a: addresses the other sidetested: mcpclient.TestModernListenStream/2026-07-28/subscriptions/resource-subscription-reopens-listen

roots

requirementlevelsidemcpx as servermcpx as client
roots-client-declare-capability-per-request-metaMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/roots/roots-client-declare-capability-per-request-meta
roots-server-list-via-input-requiredMUST (via MRTR note)servern/a: optional feature mcpx does not implementn/a: addresses the other side
roots-uri-must-be-file-schemeMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-only-expose-permitted-rootsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-uris-path-traversalMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-implement-access-controlsMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-accessibilityMUSTclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-handle-unavailable-rootsSHOULDservern/a: mcpx does not consume rootsn/a: addresses the other side
roots-server-respect-boundariesSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-validate-paths-against-rootsSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-client-prompt-consent-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-provide-root-management-uiSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-validate-accessibility-before-exposingSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-client-monitor-root-changesSHOULDclientn/a: addresses the other siden/a: mcpx exposes no roots – the daemon configures none, so roots/list is always empty and there is nothing to validate, monitor or consent to
roots-server-check-capability-before-useSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-server-cache-root-informationSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
roots-deprecated-new-impls-should-not-adoptSHOULD NOTbothn/a: optional feature mcpx does not implementtested: conformance.TestClientFeaturesClient/2026-07-28/roots/roots-deprecated-new-impls-should-not-adopt
roots-deprecated-existing-should-migrateSHOULDbothn/a: catalogue marks it n/atested: conformance.TestClientFeaturesClient/2026-07-28/roots/roots-deprecated-existing-should-migrate

sampling

requirementlevelsidemcpx as servermcpx as client
sampling-human-in-loop-can-denySHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-human-in-loop-can-deny
sampling-app-review-uiSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-view-edit-promptsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-app-review-responsesSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-client-declare-capability-per-request-metaMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-client-declare-capability-per-request-meta
sampling-client-declare-tools-capabilityMUSTclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-client-declare-tools-capability
sampling-server-no-tools-without-capabilityMUST NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-server-no-tools-without-capabilityn/a: addresses the other side
sampling-client-error-on-tools-without-capabilityMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernSamplingShape/2026-07-28/sampling/tools-without-capability-is-32602
sampling-server-includecontext-only-with-context-capabilitySHOULD NOTservertested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-server-includecontext-only-with-context-capabilityn/a: addresses the other side
sampling-server-avoid-includecontext-valuesSHOULDservergap #210: an upstream sampling request is relayed as-is: tools and includeContext reach a client that did not declare sampling.tools or sampling.contextn/a: addresses the other side
sampling-deprecated-new-impls-should-not-adoptSHOULD NOTbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-deprecated-new-impls-should-not-adopttested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-deprecated-new-impls-should-not-adopt
sampling-tool-result-message-only-tool-resultsMUSTbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-tool-result-message-only-tool-resultstested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-tool-result-message-only-tool-results
sampling-tool-use-result-balanceMUSTbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-tool-use-result-balancetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-tool-use-result-balance
sampling-server-answer-every-tooluseMUSTservertested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-server-answer-every-toolusen/a: addresses the other side
sampling-toolresult-id-must-match-tooluseMUSTbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-toolresult-id-must-match-toolusetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-toolresult-id-must-match-tooluse
sampling-toolchoice-required-model-must-use-toolMUSTclientn/a: addresses the other siden/a: mcpx never declares tools
sampling-toolchoice-none-model-must-not-use-toolsMUST NOTclientn/a: addresses the other siden/a: catalogue marks it n/a
sampling-parallel-tool-toggle-extension-onlyMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
sampling-tooluse-meta-preserveSHOULDclient (text says “Clients”; see Conflicts)n/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-tooluse-meta-preserve
sampling-toolresult-structured-conform-outputschemaSHOULDbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-toolresult-structured-conform-outputschematested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-toolresult-structured-conform-outputschema
sampling-message-must-have-role-and-contentMUSTbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-message-must-have-role-and-contenttested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-message-must-have-role-and-content
sampling-messages-not-retained-between-requestsSHOULD NOTclientn/a: addresses the other sidegap #255: terminal sampling rows are now pruned past the #1677 age bound, but a sampling request’s messages are still retained in elicit.db between separate requests until that bound; the SHOULD NOT is not met
sampling-client-must-respect-maxtokensMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-sample-fewer-tokensMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-temperature-stop-metadataMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-model-preferencesMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-modify-system-promptMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-may-ignore-includecontextMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-evaluated-in-orderMUSTclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hints-over-prioritiesSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-name-substringSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-hint-may-map-providerMAYclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-stopreason-custom-valuesMAYclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-stopreason-custom-values
sampling-client-user-approval-controlsSHOULDclientn/a: addresses the other sidegap #256: a sampling request is routed to the agent audience; nothing lets a person review or edit the prompt or the result
sampling-both-validate-message-contentSHOULDbothtested: conformance.TestSamplingValidation/2026-07-28/sampling/sampling-both-validate-message-contenttested: conformance.TestSamplingValidation/2026-07-28/sampling/sampling-both-validate-message-content
sampling-client-respect-hintsSHOULDclientn/a: addresses the other siden/a: mcpx runs no model; it relays the request unchanged to whatever answers it (the pass-through tests), which is where model choice and generation happen
sampling-client-rate-limitSHOULDclientn/a: addresses the other sidetested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-client-rate-limit
sampling-both-handle-sensitive-dataMUSTbothtested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
tested: daemon.TestElicitEventsAreSummaryOnly@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
elicit.TestOpenPrunesTerminalRowsPastTheAgeBound@2024-11-05,2025-03-26,2025-06-18,2025-11-25,2026-07-28
sampling-both-iteration-limits-tool-loopsSHOULDbothtested: conformance.TestClientFeaturesServer/2026-07-28/sampling/sampling-both-iteration-limits-tool-loopstested: conformance.TestClientFeaturesClient/2026-07-28/sampling/sampling-both-iteration-limits-tool-loops

subscriptions

requirementlevelsidemcpx as servermcpx as client
subscriptions-list-changed-via-listenSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-sse-notifications-must-relate
mcpserver.TestHTTPListenIsAStream/2026-07-28/subscriptions/http-listen-response-is-an-open-sse-stream
n/a: addresses the other side
subscriptions-only-requested-typesMUST NOTservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/concurrent-listens-are-kept-apart
mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/unrequested-type-is-never-sent
n/a: addresses the other side
subscriptions-ack-first-with-idMUST / MUST NOTservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/acknowledged-first-with-subscriptionId-in-metan/a: addresses the other side
subscriptions-stdio-ack-order-per-idMAYservern/a: catalogue marks it n/an/a: addresses the other side
subscriptions-ack-reports-agreed-subsetdescr. (schema-required field)servertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/acknowledged-first-with-subscriptionId-in-metan/a: addresses the other side
subscriptions-client-check-ackSHOULDclientn/a: addresses the other sidetested: mcpclient.TestModernListenStream/2026-07-28/subscriptions/client-checks-acknowledged-subset
subscriptions-tag-every-notificationMUSTservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/concurrent-listens-are-kept-apart
mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/every-notification-carries-the-subscriptionId
n/a: addresses the other side
subscriptions-stdio-client-correlateMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernListenStream/2026-07-28/transport-stdio/client-correlates-subscription-id
subscriptions-multiple-concurrentMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
subscriptions-server-teardown-respondSHOULDservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/server-teardown-sends-cancelled-then-resultn/a: addresses the other side
subscriptions-listen-result-metaschema-requiredservertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/server-teardown-sends-cancelled-then-resultn/a: addresses the other side
subscriptions-client-reconnect-on-dropMAYclientn/a: addresses the other siden/a: catalogue marks it n/a
subscriptions-stdio-resubscribeMUSTclientn/a: addresses the other siden/a: catalogue marks it n/a
subscriptions-request-scoped-not-on-listendescr. (bold “not”)servertested: mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/unrequested-type-is-never-sentn/a: addresses the other side

tasks

requirementlevelsidemcpx as servermcpx as client
ext-tasks-client-declare-per-requestmust (informal)clientn/a: addresses the other siden/a: optional feature mcpx does not implement
ext-tasks-server-advertise-discovermust (informal)servertested: mcpserver.TestCapabilitiesPerRevision/2026-07-28/capabilities/tasks-only-as-extensionn/a: addresses the other side
ext-tasks-never-unsolicitedmust (informal)servertested: mcpserver.TestTasksPerEra/2026-07-28/tasks/no-task-without-the-extension-task-param-ignoredn/a: addresses the other side
ext-tasks-server-decidesdescr.servertested: mcpserver.TestAQuestionAskedUpFrontIsAnsweredBeforeTheTaskExists@2026-07-28
mcpserver.TestATaskSupportingToolBecomesATaskWithoutWaitingTaskAfter@2026-07-28
mcpserver.TestTasksPerEra/2026-07-28/tasks/fast-call-is-answered-directly
mcpserver.TestTasksPerEra/2026-07-28/tasks/server-directed-CreateTaskResult-is-flat
n/a: addresses the other side
ext-tasks-durable-before-responsemust (informal)servertested: mcpserver.TestInjectedStoreKeepsMCPTasksAcrossReopen@2026-07-28n/a: addresses the other side
ext-tasks-createtaskresult-shapemust (informal)servertested: mcpserver.TestTasksPerEra/2026-07-28/tasks/server-directed-CreateTaskResult-is-flatn/a: addresses the other side
ext-tasks-get-carries-resultmust (informal)servertested: mcpserver.TestTasksPerEra/2026-07-28/tasks/server-directed-CreateTaskResult-is-flat
mcpserver.TestTasksPerEra/2026-07-28/tasks/tool-isError-is-completed-not-failed
n/a: addresses the other side
ext-tasks-get-input-requestsdescr.servertested: conformance.TestStatefulServer/2026-07-28/tasks/ext-tasks-get-input-requestsn/a: addresses the other side
ext-tasks-updatemust (informal)servertested: mcpserver.TestATaskAskingAQuestionIsInputRequiredAndTasksUpdateAnswersIt@2026-07-28
mcpserver.TestTasksPerEra/2026-07-28/tasks/update-and-cancel-ack-empty-unknown-is-32602
n/a: addresses the other side
ext-tasks-cancel-cooperativemust (informal)servergap #209: tasks/cancel forces the cancelled status rather than letting the work reach its own terminal statusn/a: addresses the other side
ext-tasks-client-polymorphicmust (informal)clientn/a: addresses the other siden/a: optional feature mcpx does not implement
ext-tasks-client-pollmust (informal)clientn/a: addresses the other siden/a: optional feature mcpx does not implement
ext-tasks-client-persist-idsmust (informal)clientn/a: addresses the other siden/a: optional feature mcpx does not implement
ext-tasks-notifications-via-listendescr. (MAY)bothtested: mcpserver.TestListenDeliversTaskStatusNotifications@2026-07-28
mcpserver.TestListenReportsATaskThatAlreadyFinished@2026-07-28
mcpserver.TestSubscriptionsListen/2026-07-28/subscriptions/taskIds-without-the-extension-is-32021
n/a: a MAY mcpx does not take: it declares no tasks extension upstream

tools

requirementlevelsidemcpx as servermcpx as client
tools-human-in-loop-denySHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-ui-expose-indicate-confirmSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-declare-capabilityMUSTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-declare-capabilityn/a: addresses the other side
tools-list-changed-means-emit(definition)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-changed-means-emitn/a: addresses the other side
tools-list-changed-notify-to-listenersSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-changed-notify-to-listenersn/a: addresses the other side
tools-list-must-respond-when-declaredMUSTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-must-respond-when-declaredn/a: addresses the other side
tools-list-not-per-connectionMUST NOTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-not-per-connectionn/a: addresses the other side
tools-list-may-vary-by-authMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-list-may-be-empty-changeMAYservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-may-be-empty-changen/a: addresses the other side
tools-list-deterministic-orderSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-list-deterministic-ordern/a: addresses the other side
tools-annotations-untrustedMUSTclientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-annotations-hints-no-decisionsshould (lowercase, schema)clientn/a: addresses the other sidetested: daemon.TestAnnotationClearsCallOnlyForLocallyDeclaredServers@2026-07-28
tools-inputschema-no-params-recommendedRECOMMENDEDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-inputschema-no-params-recommendedn/a: addresses the other side
tools-x-mcp-header-mayMAYservern/a: optional feature mcpx does not implementn/a: addresses the other side
tools-x-mcp-header-not-emptyMUST NOTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-tcharMUSTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-no-ctlMUST NOTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-unique-ciMUSTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-primitive-onlyMUSTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-int-safe-rangeMUSTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-statically-reachableMUSTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-x-mcp-header-reject-excludesMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernHTTPHeaders/2026-07-28/transport/client-excludes-tool-with-invalid-x-mcp-header
tools-x-mcp-header-reject-logSHOULDclientn/a: addresses the other sidetested: mcpclient.TestModernHTTPHeaders/2026-07-28/transport/client-excludes-tool-with-invalid-x-mcp-header
tools-x-mcp-header-stdio-may-ignoreMAYclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/tools/tools-x-mcp-header-stdio-may-ignore
tools-x-mcp-header-no-secretsSHOULD NOTservern/a: no tool mcpx serves declares an x-mcp-header parametern/a: addresses the other side
tools-name-lengthSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-name-lengthn/a: addresses the other side
tools-name-case-sensitiveSHOULDbothtested: conformance.TestFeaturesServer/2026-07-28/tools/tools-name-case-sensitivetested: conformance.TestFeaturesClient/2026-07-28/tools/tools-name-case-sensitive
tools-name-charsetSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-name-charsetn/a: addresses the other side
tools-name-no-specialSHOULD NOTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-name-no-specialn/a: addresses the other side
tools-name-unique-in-serverSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-name-unique-in-servern/a: addresses the other side
tools-aggregator-disambiguateSHOULDclientn/a: addresses the other sidetested: e2e.TestAliasExposesASubsetOfTheSameServer@2026-07-28
e2e.TestDuplicateNamespaceIsRejected@2026-07-28
tools-aggregator-not-by-servernameSHOULD NOTclientn/a: addresses the other sidetested: e2e.TestDuplicateNamespaceIsRejected@2026-07-28
tools-result-iserror-for-tool-errorsSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-result-iserror-for-tool-errorsn/a: addresses the other side
tools-result-lookup-errors-protocolshould (lowercase)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-result-lookup-errors-protocoln/a: addresses the other side
tools-error-unknown-tool-protocol(classification)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-error-unknown-tool-protocoln/a: addresses the other side
tools-error-input-validation-execution(classification)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-error-input-validation-executionn/a: addresses the other side
tools-client-exec-errors-to-llmSHOULDclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/tools/tools-client-exec-errors-to-llm
tools-client-protocol-errors-to-llm-mayMAYclientn/a: addresses the other sidetested: conformance.TestFeaturesClient/2026-07-28/tools/tools-client-protocol-errors-to-llm-may
tools-embedded-resource-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-embedded-resource-mayn/a: addresses the other side
tools-embedded-implies-resources-capSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-embedded-implies-resources-capn/a: addresses the other side
tools-resource-link-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-resource-link-mayn/a: addresses the other side
tools-resource-link-may-not-listed(note)bothtested: conformance.TestFeaturesServer/2026-07-28/tools/tools-resource-link-may-not-listedtested: conformance.TestFeaturesClient/2026-07-28/tools/tools-resource-link-may-not-listed
tools-structured-also-textSHOULDservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-structured-also-textn/a: addresses the other side
tools-structured-any-json(type)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-structured-any-jsonn/a: addresses the other side
tools-output-schema-conformMUSTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-output-schema-conformn/a: addresses the other side
tools-output-schema-client-validateSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
tools-display-name-precedence(rule)clientn/a: addresses the other sidegap #207: tool title and annotations.title are carried but nothing that displays a tool uses them
tools-server-sec-validate-inputsMUSTservern/a: optional feature mcpx does not implementn/a: addresses the other side

tools / json-schema

requirementlevelsidemcpx as servermcpx as client
tools-inputschema-valid-not-nullMUSTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-inputschema-valid-not-nulln/a: addresses the other side
tools-inputschema-type-object-rootREQUIRED (schema)servertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-inputschema-type-object-rootn/a: addresses the other side
tools-client-sec-ref-resolutionSHOULDclientn/a: addresses the other siden/a: mcpx never validates arguments against an upstream inputSchema, so it resolves no $ref for validation

tools / mrtr

requirementlevelsidemcpx as servermcpx as client
tools-call-input-required-mayMAYservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-call-input-required-mayn/a: addresses the other side
tools-call-inputrequests-declared-onlyMUST NOTservertested: conformance.TestFeaturesServer/2026-07-28/tools/tools-call-inputrequests-declared-onlyn/a: addresses the other side

transport-http-sse

requirementlevelsidemcpx as servermcpx as client
http-sse-deprecated-no-new-adoptionSHOULD NOTbothtested: conformance.TestTransportServer/2026-07-28/transport-http-sse/http-sse-deprecated-no-new-adoptiontested: conformance.TestTransportClient/2026-07-28/transport-http-sse/http-sse-deprecated-no-new-adoption
http-sse-server-backcompat-host-bothshould (lowercase)servern/a: optional feature mcpx does not implementn/a: addresses the other side
http-sse-client-backcompat-probe-non-modern-bodyshould (lowercase)clientn/a: addresses the other sidetested: mcpclient.TestHTTPEraProbe/2026-07-28/http-compat/legacy-refused-too-is-typed-for-http-sse

transport-stdio

requirementlevelsidemcpx as servermcpx as client
transport-custom-preserve-patterns-and-metadataMUSTbothn/a: no custom MCP transportn/a: catalogue marks it n/a
transport-custom-document-patternsSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
transport-custom-byte-stream-reuses-stdio-framingSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
stdio-no-embedded-newlinesMUST NOTbothtested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-no-embedded-newlinestested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-no-embedded-newlines
stdio-message-is-singleMUST (implied)bothtested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-message-is-singletested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-message-is-single
stdio-server-may-log-to-stderrMAYservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-may-log-to-stderrn/a: addresses the other side
stdio-client-may-capture-stderrMAYclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-may-capture-stderr
stdio-client-stderr-not-errorSHOULD NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-stderr-not-error
stdio-server-stdout-only-mcpMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-stdout-only-mcpn/a: addresses the other side
stdio-client-stdin-only-mcpMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-stdin-only-mcp
stdio-client-no-responsesMUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-no-responses
stdio-server-no-requestsMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-no-requestsn/a: addresses the other side
stdio-client-correlates-subscription-idMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernListenStream/2026-07-28/transport-stdio/client-correlates-subscription-id
stdio-client-cancel-by-notificationMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-cancel-by-notification
stdio-server-stop-cancelled-workSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-stop-cancelled-workn/a: addresses the other side
stdio-server-silent-after-cancelMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-silent-after-canceln/a: addresses the other side
stdio-server-cancelled-only-for-listenMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-cancelled-only-for-listenn/a: addresses the other side
stdio-client-shutdown-sequenceSHOULDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-client-shutdown-sequence
stdio-server-exit-on-eofSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-exit-on-eofn/a: addresses the other side
stdio-server-may-close-stdoutMAYservertested: conformance.TestTransportServer/2026-07-28/transport-stdio/stdio-server-may-close-stdoutn/a: addresses the other side
stdio-client-restart-crashed-serverSHOULDclientn/a: addresses the other sidegap #257: unverified: the in-flight call fails and the pool respawns only on the next acquire, subject to its start-failure cooldown
stdio-dual-era-client-probe-discoverSHOULDclientn/a: addresses the other siden/a: optional feature mcpx does not implement
stdio-no-fallback-on-modern-errorMUST NOT (informal “Do not”)clientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-no-fallback-on-modern-error
stdio-modern-only-client-probeRECOMMENDEDclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-stdio/stdio-modern-only-client-probe

transport-streamable-http

requirementlevelsidemcpx as servermcpx as client
streamable-http-server-single-endpoint-postMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-single-endpoint-postn/a: addresses the other side
streamable-http-server-validate-originMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-validate-originn/a: addresses the other side
streamable-http-server-403-invalid-originMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-403-invalid-originn/a: addresses the other side
streamable-http-server-bind-localhostSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-bind-localhostn/a: addresses the other side
streamable-http-server-authenticateSHOULDservern/a: optional feature mcpx does not implementn/a: addresses the other side
streamable-http-client-post-every-messageMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-client-post-every-message
streamable-http-client-accept-bothMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-client-accept-both
streamable-http-post-body-request-or-notificationMUST / MUST NOTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-post-body-request-or-notification
streamable-http-client-sends-metadata-headersMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernHTTPHeaders/2026-07-28/transport/client-sends-standard-and-param-headers
streamable-http-server-202-for-non-requestMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-202-for-non-requestn/a: addresses the other side
streamable-http-server-error-status-if-rejectedMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-error-status-if-rejectedn/a: addresses the other side
streamable-http-server-json-or-sse-for-requestMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-json-or-sse-for-requestn/a: addresses the other side
streamable-http-client-supports-json-and-sseMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-client-supports-json-and-sse
streamable-http-sse-notifications-must-relateMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-sse-notifications-must-relaten/a: addresses the other side
streamable-http-sse-no-requestsMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-sse-no-requestsn/a: addresses the other side
streamable-http-server-terminates-sse-after-responseSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-terminates-sse-after-responsen/a: addresses the other side
streamable-http-close-is-cancelMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-close-is-canceln/a: addresses the other side
streamable-http-stop-cancelled-workSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-stop-cancelled-workn/a: addresses the other side
streamable-http-silent-after-cancelMUST NOTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-silent-after-canceln/a: addresses the other side
streamable-http-no-resumability— (statement)bothtested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-no-resumabilitytested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-no-resumability
streamable-http-client-reissue-broken-requestMUST (changelog)clientn/a: addresses the other sidetested: mcpclient.TestModernReissueBrokenRequest/2026-07-28/transport/client-reissues-broken-request-once
streamable-http-protocol-version-header-every-postMUSTclientn/a: addresses the other sidetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-protocol-version-header-every-post
streamable-http-protocol-version-header-matches-metaMUSTbothtested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-protocol-version-header-matches-metatested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-protocol-version-header-matches-meta
streamable-http-unsupported-version-400MUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-unsupported-version-400n/a: addresses the other side
streamable-http-missing-capability-400MUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-missing-capability-400n/a: addresses the other side
streamable-http-unknown-method-404MUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-unknown-method-404n/a: addresses the other side
streamable-http-missing-version-header-legacyMAY / MUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-missing-version-header-legacyn/a: addresses the other side
streamable-http-standard-headers-requiredREQUIREDclientn/a: addresses the other sidetested: mcpclient.TestTheVersionHeaderFollowsTheFrame/2026-07-28/transport/standard-headers-mcp-method-and-mcp-name
streamable-http-mcp-name-base64MUSTclientn/a: addresses the other sidetested: mcpclient.TestHeaderValueEncoding/2026-07-28/transport/value-encoding-examples
streamable-http-x-mcp-header-server-mayMAYservern/a: but mcpx re-exports upstream tool schemas — an upstream x-mcp-header passes through, obliging mcpx’s HTTP clients and mcpx’s own validation (see Impliedn/a: addresses the other side
streamable-http-x-mcp-header-client-must-mirrorMUSTclientn/a: addresses the other sidetested: mcpclient.TestParamHeaders/2026-07-28/transport/param-headers-mirror-and-omit-null
streamable-http-x-mcp-header-constraintsMUST / MUST NOTservern/a: unless passing through upstream annotationsn/a: addresses the other side
streamable-http-client-rejects-invalid-x-mcp-headerMUSTclientn/a: addresses the other sidetested: mcpclient.TestModernHTTPHeaders/2026-07-28/transport/client-excludes-tool-with-invalid-x-mcp-header
mcpclient.TestToolHeaderAnnotations/2026-07-28/transport/client-rejects-invalid-x-mcp-header
streamable-http-param-value-encodingMUSTclientn/a: addresses the other sidetested: mcpclient.TestHeaderValueEncoding/2026-07-28/transport/value-encoding-examples
streamable-http-server-decodes-before-compareMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-decodes-before-comparen/a: addresses the other side
streamable-http-client-header-constructionMUSTclientn/a: addresses the other sidetested: mcpclient.TestParamHeaders/2026-07-28/transport/param-headers-mirror-and-omit-null
streamable-http-client-relist-on-header-mismatchSHOULDclientn/a: addresses the other sidetested: mcpclient.TestModernHTTPHeaders/2026-07-28/transport/client-relists-and-retries-on-header-mismatch
streamable-http-intermediary-forwards-unknown-paramMUSTintermediaryn/a: mcpx is not an HTTP intermediary; it terminates MCP and re-originatesn/a: catalogue marks it n/a
streamable-http-server-validates-param-headersMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-validates-param-headersn/a: addresses the other side
streamable-http-header-names-case-insensitiveMUSTbothtested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-header-names-case-insensitivetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-header-names-case-insensitive
streamable-http-server-rejects-header-body-mismatchMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-server-rejects-header-body-mismatchn/a: addresses the other side
streamable-http-header-mismatch-400-jsonrpcMUSTservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-header-mismatch-400-jsonrpcn/a: addresses the other side
streamable-http-numeric-compareSHOULDservern/a: catalogue marks it n/an/a: addresses the other side
streamable-http-intermediary-http-errorMUSTintermediaryn/a: catalogue marks it n/atested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-intermediary-http-error
streamable-http-intermediary-reject-unvalidated-versionSHOULDintermediaryn/a: catalogue marks it n/an/a: catalogue marks it n/a
streamable-http-x-accel-bufferingSHOULDservertested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-x-accel-bufferingn/a: addresses the other side
streamable-http-sse-comment-keepaliveshould/must (lowercase)bothtested: conformance.TestTransportServer/2026-07-28/transport-streamable-http/streamable-http-sse-comment-keepalivetested: conformance.TestTransportClient/2026-07-28/transport-streamable-http/streamable-http-sse-comment-keepalive
streamable-http-modern-only-server-legacy-trafficSHOULDservern/a: as written (mcpx is dual-era, not modern-only) — but mcpx mints a session on server/discover for modern clients by choice (to bind requestStaten/a: addresses the other side

versioning

requirementlevelsidemcpx as servermcpx as client
versioning-implementations-support-base-versioning-patternsMUSTbothtested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-implementations-support-base-versioning-patternstested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-implementations-support-base-versioning-patterns
meta-extension-ids-follow-meta-rulesMUSTbothtested: conformance.TestLifecycleServer/2026-07-28/versioning/meta-extension-ids-follow-meta-rulesn/a: declares no extensions
versioning-server-unsupported-version-errorMUSTservertested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-server-unsupported-version-errorn/a: addresses the other side
versioning-client-retry-with-supportedSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-client-retry-with-supported
mcpclient.TestVersionRetry/2026-07-28/versioning/client-retries-with-supported-version
versioning-server-must-implement-discoverMUSTservertested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-server-must-implement-discovern/a: addresses the other side
versioning-client-may-discover-firstMAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-client-may-discover-first
versioning-extension-fallback-or-rejectMUSTbothtested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-extension-fallback-or-rejectn/a: catalogue marks it n/a
versioning-extensions-document-fallbackSHOULDbothn/a: catalogue marks it n/an/a: catalogue marks it n/a
versioning-dual-era-server-mayMAYservertested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-dual-era-server-mayn/a: addresses the other side
versioning-era-cacheSHOULD / MAYclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-era-cache
versioning-modern-only-server-names-versions-to-initializeSHOULDservern/a: dual-era); for an initialize naming an unsupported version it does name the legacy listn/a: addresses the other side
versioning-modern-client-probe-legacy-stdioSHOULDclientn/a: addresses the other siden/a: unless protocol: force-discover; then discover is first (yes
versioning-dual-era-client-probe-discover-first-stdioSHOULDclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-dual-era-client-probe-discover-first-stdio
versioning-fallback-not-keyed-to-one-codeMUST NOTclientn/a: addresses the other sidetested: conformance.TestLifecycleClient/2026-07-28/versioning/versioning-fallback-not-keyed-to-one-code
versioning-dual-era-http-detectMAY / SHOULDclientn/a: addresses the other siden/a: legacy first
versioning-dual-era-server-selects-by-opening(normative description)servertested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-dual-era-server-selects-by-openingn/a: addresses the other side
versioning-dual-era-both-concurrentlyMAYservertested: conformance.TestLifecycleServer/2026-07-28/versioning/versioning-dual-era-both-concurrentlyn/a: addresses the other side
deprecated-should-not-adoptSHOULD NOT / SHOULDbothtested: conformance.TestLifecycleServer/2026-07-28/versioning/deprecated-should-not-adopttested: conformance.TestLifecycleClient/2026-07-28/versioning/deprecated-should-not-adopt

Authorization rows: accepts, or requires refusal (#1972)

61 authorization rows are split per side of the wire into 82 bound row-sides: 69 permissive and 13 validation. A permissive row-side is satisfied by an interface that accepts every credential (#1950) — endpoint shape, discovery documents, registration, flows. A validation row-side cannot pass against such an interface: it needs a real check, named below, to refuse or challenge a credential (#1972).

Validation row-sides per check: invalid 6, expired 5, wrong-resource 3, scope 5. Input: internal/conformance/authsplit.json; edits belong there, never in this file.

requirementsideverdict
authorization-http-should-conform-stdio-should-notserverpermissive
authorization-http-should-conform-stdio-should-notclientpermissive
authorization-custom-auth-mayserverpermissive
authorization-custom-auth-mayclientpermissive
streamable-http-auth-discovery-version-headerclientpermissive
authz-optionalserverpermissive
authz-optionalclientpermissive
authz-http-should-conformserverpermissive
authz-http-should-conformclientpermissive
authz-stdio-use-environmentserverpermissive
authz-stdio-use-environmentclientpermissive
authz-alt-transport-best-practiceserverpermissive
authz-alt-transport-best-practiceclientpermissive
authz-implement-oauth21serverpermissive
authz-implement-oauth21clientpermissive
authz-dcr-shouldserverpermissive
authz-dcr-shouldclientpermissive
authz-dcr-may-deprecatedclientpermissive
authz-rfc8414-server-should-client-mustserverpermissive
authz-rfc8414-server-should-client-mustclientpermissive
authz-grant-typesserverpermissive
authz-401-when-unauthorizedservervalidation: invalid — an unproven or invalid credential must be refused with 401
authz-base-url-discards-pathclientpermissive
authz-default-endpoints-fallbackclientpermissive
authz-bearer-headerclientpermissive
authz-token-every-requestclientpermissive
authz-no-token-in-queryclientpermissive
authz-server-validates-tokenservervalidation: invalid, expired — tokens are validated; invalid or expired ones must get 401
authz-server-validates-audienceservervalidation: wrong-resource — the token must be issued for this server as audience
authz-client-sends-only-own-as-tokensclientpermissive
authz-accept-only-own-tokensservervalidation: wrong-resource — only tokens valid for this server’s resources may be accepted
authz-no-token-passthroughserverpermissive
authz-error-status-codesservervalidation: invalid, expired, wrong-resource, scope — the refusal status code (401/403/400) for every authorization error
authz-2025-03-26-security-listservervalidation: invalid, expired — servers enforce token expiration and validate redirect URIs
authz-2025-03-26-security-listclientpermissive
authz-2025-03-26-implementation-reqsservervalidation: expired — token rotation and limited lifetimes are enforced server-side
authz-2025-03-26-implementation-reqsclientpermissive
authz-third-party-session-bindingservervalidation: invalid, expired — third-party token status validated and expiration handled
authz-server-prm-rfc9728serverpermissive
authz-server-prm-rfc9728clientpermissive
authz-prm-lists-authorization-serversserverpermissive
authz-as-metadata-rfc8414serverpermissive
authz-as-metadata-rfc8414clientpermissive
authz-as-metadata-rfc8414-or-oidcserverpermissive
authz-as-metadata-rfc8414-or-oidcclientpermissive
authz-401-www-authenticateservervalidation: invalid — the 401 refusal must carry the WWW-Authenticate metadata pointer
authz-prm-discovery-header-or-well-knownserverpermissive
authz-prm-discovery-header-or-well-knownclientpermissive
authz-client-parses-www-authenticateclientpermissive
authz-client-rfc8414-discoveryclientpermissive
authz-client-as-discovery-orderclientpermissive
authz-client-validates-issuer-in-metadataclientpermissive — metadata is checked deterministically; no credential is inspected
authz-client-per-as-registration-stateclientpermissive
authz-resource-indicatorclientpermissive
authz-canonical-uri-guidanceserverpermissive
authz-canonical-uri-guidanceclientpermissive
authz-secure-token-storageserverpermissive
authz-secure-token-storageclientpermissive
authz-https-and-redirect-urisserverpermissive
authz-https-and-redirect-urisclientpermissive
authz-pkceclientpermissive
authz-pkce-verify-s256clientpermissive — the client refuses to proceed on AS capability, not on a presented credential
authz-open-redirectionclientpermissive
authz-proxy-consent-per-clientserverpermissive — user consent is a flow gate, not a credential check; no credential is inspected
authz-client-registration-requiredclientpermissive
authz-registration-priorityclientpermissive
authz-cimdclientpermissive
authz-preregistrationclientpermissive
authz-dcr-application-typeclientpermissive
authz-credentials-bound-to-issuerclientpermissive
authz-scope-in-challengeservervalidation: scope — the refusal challenge must name the scope required
authz-scope-in-challengeclientpermissive
authz-scope-selection-orderclientpermissive
authz-insufficient-scope-403servervalidation: scope — insufficient scope must be refused with 403 insufficient_scope
authz-all-scopes-single-challengeservervalidation: scope — one refusal challenge listing every scope the operation needs
authz-step-upclientpermissive — the client reacts to a refusal it is given; it validates nothing itself
authz-server-scope-hierarchyservervalidation: scope — scope hierarchies must be accounted for when enforcing scope
authz-iss-validationclientpermissive — discovery/issuer metadata is checked deterministically; no credential is inspected
authz-refresh-tokensserverpermissive
authz-refresh-tokensclientpermissive
authz-follow-security-considerationsserverpermissive
authz-follow-security-considerationsclientpermissive

The conformance matrix

docs/spec/conformance-matrix.md says, for every normative requirement of every MCP revision from 2024-11-05 to 2026-07-28, and for each side mcpx plays, whether a named test holds mcpx to it, why it does not bind mcpx, or which issue tracks mcpx not meeting it. go test ./internal/conformance fails when any cell has none of the three. This is how it is put together, and what building it found.

Pieces

  • Catalogue. docs/spec/requirements.md is the prose catalogue (788 rows, from the five area sweeps). go run ./internal/conformance/cmd/genreq turns it into internal/conformance/requirements.json: revision ranges expanded, the side column read as server/client applicability. Anything the catalogue does not plainly mark n/a is taken to apply – a row wrongly marked as applying shows up as a missing test and gets looked at; one wrongly marked n/a is never looked at again. Re-judgements live in overrides/*.json, each with its reason. Authorization rows are not-implemented:#253: mcpx implements no OAuth.
  • Coverage. coverage_*.go, one file per area. A cover names a test as <pkg>.<TestName>/<subtest>; the subtest’s leading revision is the revision it covers, or an explicit @rev,... for a test not named per revision. SReq/CReq cover the requirement-named subtests forReq produces.
  • Gaps. A Gap names a requirement, side, optional revisions, an issue, and why. The tests in this package consult the gap list at run time: a gapped cell’s subtest skips with gap: <issue>, so the failing check is kept and runs again the moment the entry is removed. MCPX_CONFORMANCE_RUN_GAPS=1 runs them all anyway – which is how the gaps closed by #239 were found and removed.
  • The check. TestMatrix regenerates requirements.json and the matrix and compares both with the committed files (MCPX_UPDATE_MATRIX=1 rewrites them), parses the Go test sources to refuse a cover that names a test which does not exist or which skips as a gap, and fails listing every cell with no cover and no gap. It also fails when a tested cell’s catalogue note claims a gap (any gap/GAP in the note, recorded as claims in requirements.json): the note and the test cannot both be right, so neither is allowed to silently outrank the other. The matrix names the sha256 of its inputs (the catalogue, overrides and coverage*.go) rather than a commit, since a file cannot name the commit that contains it; the comparison is what keeps it current. MCPX_MATRIX_AREAS=tools,resources narrows the list. go run ./internal/conformance/cmd/covering runs exactly the covering tests, one go test -run per package (-n prints the commands).

Why forReq takes its revisions from the catalogue

A test that loops over its own list of revisions can claim one it never runs. forReq(t, side, id, fn) reads the requirement’s revisions from requirements.json and runs fn once per revision as <rev>/<area>/<id>, which is exactly the name the cover expands to. The cover and the run cannot drift.

The harness

harness_test.go is shared by every area: mcpx’s server over stdio pipes (stdioServer) and over a real listener (httpServer), with a complete fake backend, a filtering notifier and a scriptable Asker; mcpx’s client against a scripted peer of any revision (newPeer), in process or behind Streamable HTTP (httpPeer). Every frame either side emits is validated with mcpspec – strictly for mcpx’s server frames, so a key a revision does not define fails the test that provoked it.

What building it found and fixed (internal/mcpserver)

Each fix has a test in this package that fails without it.

  • A question mcpx put to a legacy client was not bounded by the ask deadline: a client that never answered held the call for good, and the question was never cancelled. It is now asked under the deadline, and its expiry sends notifications/cancelled.
  • A batch of 2026-07-28 requests on a stdio process that had not been initialized was answered as a 2025-03-26 batch. An element that names its own revision now decides.
  • logging/setLevel with a level that is not one of the eight answered {}; it is -32602 now. A valid level is still accepted though mcpx sends no log messages.
  • completion.maxValues could raise a completion past the 100 values every revision allows.
  • The batch refusal was two sentences; error messages are one.
  • An invalid list cursor restarted at the first page; it is -32602 now, in every list including tasks/list.
  • (internal/cli/serve.go) mcpx://<ns>/<uri> concatenated the upstream URI unencoded, so a space, # or ? in it made something that is not a URI, or not the one meant. Characters a path may not hold are percent-encoded, % included, and decoded again on read, completion and subscribe.
  • tasks/cancel of a finished task succeeded; 2025-11-25 makes it -32602.
  • An unknown tool name was an isError result; it is -32602, before any task or question starts.
  • prompts/get did not check required arguments; a missing one is now -32602 before anything runs or asks. (The -32602/-32603 classification of backend failures landed at the same time in #245; the two are merged.) The missing backend == nil guard is added.

Surprises

  • Through 2025-06-18 the schema requires an error response’s id to be a string or integer, and JSON-RPC requires null when the request could not be read. No frame satisfies both; mcpx omits the id (C4 in revision-conflicts.md).
  • 2026-07-28 removed ping. mcpx still answers one (accept liberally) and its client no longer sends one.
  • Two ServeStdio loops on one Server shared its default connection, so a question could reach the wrong one. No process does this – one stdio server per process – but a test that tried it did; a second loop now gets its own.

Upstream era probe

How mcpx decides whether a server it fronts speaks the modern (2026-07-28, per-request _meta) protocol or a legacy (initialize) one, and how it remembers the answer.

Sources: versioning, stdio, Streamable HTTP.

Values

upstream.protocol, and a server’s own protocol key, which overrides it, take one of the values below. Each is named for the first request mcpx sends, not for an era’s relative age; the older names are still accepted wherever the value is read (config file, MCPX_UPSTREAM_PROTOCOL, --upstream-protocol, PUT /v1/settings) and are normalised to the canonical one, which is what mcpx settings, mcpx config and GET /v1/protocol report. Case does not matter. Any other value is refused with this list.

canonicalmeaningaliases (accepted, normalised to the canonical)
prefer-discover (default)server/discover first, fall back to initializemodern, prefer-modern, prefer-stateless, prefer-newest
prefer-initializeinitialize first, fall back to server/discoverlegacy, prefer-legacy, prefer-session, prefer-oldest
force-discoverserver/discover onlyforce-modern, force-stateless
force-initializeinitialize onlyforce-legacy, force-session
followunchanged—

What each value does against a server that speaks only the old (initialize, 2025-11-25 and earlier) protocol, both, or only the new (server/discover, 2026-07-28) one. “Legacy session” means an initialize handshake; “modern session” means per-request _meta with no handshake. With the era cache on, the prefer-* and follow values try the cached era first on later starts; the result is the same.

valueold-only serverdual-version servernew-only server
prefer-discoverdiscover is refused (or unanswered within upstream.probeTimeout on stdio), then initialize: legacy sessiondiscover answers: modern sessionmodern session
prefer-initializelegacy session, no discover sentinitialize answers: legacy session, so the server’s 2026-07-28 features are not usedinitialize is refused, then discover: modern session (fails if a stdio server exits on the refused initialize)
force-discoverfails to connect; no fallbackmodern sessionmodern session
force-initializelegacy sessionlegacy sessionfails to connect; no fallback
followas prefer-discover: one legacy session, shared by every callermodern session for 2026-07-28 callers plus a second, legacy session for callers on an older revision (Follow)modern session; the legacy session is refused, so older callers share the modern one

Order

The default is discover first (upstream.protocol=prefer-discover; a server’s own protocol key overrides it). Legacy first used to be the default on the argument that nearly every server is legacy and the probe costs each of them a round trip. The spec prescribes the opposite order for a dual-era client, and the era cache turns the probe into a one-time cost per server configuration, so the argument no longer holds.

Follow

protocol: follow (per server, or upstream.protocol=follow) is prefer-discover for the server’s own session, plus a second, legacy-only session (force-initialize – initialize, no probe) used for any call whose caller reached mcpx in a revision before 2026-07-28. The point is server-to-client requests: a dual-era server can push elicitation/create or sampling/createMessage only on a legacy session, and a legacy caller can only be asked by a request. Over the modern session such a server answers its legacy-only tools -32601 (the official suite’s test_elicitation, test_sampling).

  • Who is legacy. The caller’s revision travels with the call: mcpx’s MCP server records it per request, mcpx serve sends it to the daemon as X-Mcpx-Caller-Protocol on /v1/call and /v1/ask, and the pool reads it. A call with no caller revision (the CLI, /v1 direct, schema refresh) is modern.
  • Pool. The two sessions are two instances under the same scope key; the key is still the caller’s identity, and the era is a second axis. Each lane has its own max, so a global scope (max 1) holds one of each, and a legacy call the server never finishes cannot stall modern callers. A legacy caller also takes a modern-lane instance that settled legacy (a legacy-only server needs no second session). Eviction stays within a lane.
  • Fallback. If the legacy-only start fails (a modern-only server refuses initialize), the pool remembers it for its lifetime and legacy callers share the modern session, as under prefer-discover.
  • Era cache. The legacy session is forced, so it neither reads nor writes the cache; the cache keeps describing the server’s own era.
  • Attribution. A legacy HTTP server’s request on a POST’s response stream is handed to the answer handler with that request’s context, so the daemon attributes it to the call that provoked it rather than inferring it from who else shares the session (it could not when two calls were in flight).
  • Why not the default. For a stdio server the second session is a second process for the same key – a second browser, a second index – which the pool otherwise never does. Opt in where the server is cheap to run twice or where legacy callers need its questions.

stdio

server/discover is sent with the full _meta (newest version mcpx offers, client capabilities, client info). Then:

answereranext
a result with a supportedVersions arraymodernnewest mutual version; none mutual is an error
-32020, -32021, -32022modern-32022 retries with the newest mutual version from data.supported, each version at most once; otherwise an error. Never initialize.
any other errorlegacyinitialize + notifications/initialized
a success that is not a DiscoverResultlegacya catch-all handler answered under legacy semantics
nothing within upstream.probeTimeoutundecidedinitialize is sent alongside the pending discover; whichever settles the era first wins
the connection closeslegacyErrClosedDuringProbe; the pool starts a new process and connects legacy-only

The timeout does not abandon the discover because the commonest slow server is a modern one being downloaded by npx. Giving up on its answer at 2s would classify it legacy and then cache that.

Streamable HTTP

The probe is a POST of server/discover with MCP-Protocol-Version equal to the _meta version and no session id. A non-2xx body that is a JSON-RPC response whose id matches the request is delivered as that response, so a -32022 reaches the retry logic. Otherwise the transport returns *HTTPStatusError (status, body, parsed JSON-RPC error). A 4xx with a recognised modern error is modern; any other 4xx falls back to initialize; connection failures and 5xx are errors and nothing is cached. When initialize is itself refused with a bare 400/404/405 the error is a *LegacyHTTPRefusedError, which is the hook for falling back to the deprecated HTTP+SSE transport (not implemented here). There is no probe timeout on HTTP: status codes decide, and a slow server is only slow.

Era cache

Key: a SHA-256 of the process definition only – command, args, env, cwd, transport, url, headers (JSON-encoded, so map keys are sorted). Not PoolID(): that includes leasing knobs and hashes headers in map iteration order, so it differs between runs.

Value: era, negotiated version, when, and how it was learned. Stored in memory and in <state>/upstream-eras.json (upstream.eraCache, default on), written atomically (temp file + rename, mode 0600) with a version field. A missing, truncated, non-JSON or other-version file is treated as empty and rewritten on the next start.

A cached legacy server gets initialize first and no discover at all. A cached modern server gets the normal probe. If the cached era turns out wrong the probe continues in the normal order, the cache is rewritten, and the pool emits server.era.stale. force-* never reads or writes the cache.

GET /v1/protocol reports per upstream server the effective preference, the live era and negotiated version, eraSource (probe, cache, forced), and the cached record.

Who an MCP request is from

mcpx’s own MCP server passes every call on to an upstream server, and which instance of that server answers depends on its scope: one per session, per cwd, per pid, per repository, or per call. So every request has to answer the question “who is asking?”, and the answer has to come from the transport, not from the process.

What went wrong

The backend used to ask the process. mcpSession() returned $MCPX_SESSION_ID or mcp-<pid>, and callContext() filled in the process’s cwd and pid. That is right for mcpx serve, where one process serves one client over stdio. It is wrong inside the daemon, which serves /mcp for every HTTP client from one process: every client, every Mcp-Session-Id, resolved to session:mcp-<daemonpid>, cwd:<daemon's cwd> and pid:<daemonpid>. Two agents talking to one daemon shared one browser, one REPL, one set of cookies, and saw each other’s mcpx_exec artifacts in resources/list (conflict #2 in the WP7 record). Over stdio the same test gave separate instances, so the two transports disagreed about what scope: session means.

What mcpx does now

mcpserver resolves an Identity for each request from the connection it arrived on, and puts it in the request’s context (internal/mcpserver/identity.go). The backend builds the daemon call context from that (mcpCaller in internal/cli/serve.go):

request arrives onidentitysessioncwd / pid
stdio (mcpx serve)the process$MCPX_SESSION_ID, else mcp-<pid>the process’s
a legacy Streamable HTTP sessionits Mcp-Session-Idmcp-<session id>unknown
any request carrying _meta["dev.mcpx/session"]that namethe name, verbatimunknown
any request carrying _meta["ai.opencode/sessionID"] (and no dev.mcpx/session)that idthe id, verbatimunknown
a 2026-07-28 request without it, or a legacy POST outside a sessionnonenone: a fresh call idunknown

The rows are in precedence order except that an explicit session key wins over the connection’s own identity. When a request carries more than one listed key, the first key in transport.sessionMetaKeys order whose value is a non-empty string wins.

Why cwd and pid are unknown over HTTP

The daemon’s cwd and pid are not the client’s, and the transport carries neither. A scope that needs one (cwd, repo, worktree, pid) degrades to per-call and logs the degradation once, which is the direction that can’t leak one client’s state into another. A client’s roots would be a better source for cwd. That needs a roots/list round trip that mcpx doesn’t make yet.

Why a modern request gets no identity

2026-07-28 has no sessions. The streamable-http page tells a server to ignore Mcp-Session-Id and not to mint one, so no connection-level identity exists. mcpx doesn’t invent one: an unnamed request is its own scope, which for a session-scoped upstream means a new instance per request.

The session keys: transport.sessionMetaKeys

For a client that wants continuity, such as an agent host that keeps one browser across its calls, mcpx reads a session name (a string) from any request of any revision, under the keys listed in the transport.sessionMetaKeys setting, in order. The default list is dev.mcpx/session — mcpx’s own key, which uses mcpx’s prefix because the specification reserves io.modelcontextprotocol/ and defines no session key of its own — followed by ai.opencode/sessionID, the key OpenCode v2 sends on every tools/call, so each OpenCode session is scoped as its own session instead of sharing the daemon’s. A deployment can replace the list (dropping either key, or adding a host’s own) in configuration; an empty or non-string value is ignored, as if the key were absent. The value is used verbatim as the session id, so a client can join the same session a CLI user names with --session or $MCPX_SESSION_ID. That is deliberate. Anyone who can reach the daemon can already run any tool as its user (see the warning the daemon prints for a non-loopback address), so a session name is a label and grants no authority.

ai.opencode/sessionID and transport.sessionMetaKeys

opencode v2 puts _meta["ai.opencode/sessionID"] on every tools/call, including calls made from inside its code mode, so a client that never injects MCPX_SESSION_ID can still get one stateful scope per opencode session. mcpx reads a precedence list of _meta keys, transport.sessionMetaKeys (default dev.mcpx/session,ai.opencode/sessionID): the first key present on a request wins, and an empty value counts as absent. dev.mcpx/session is first so a name chosen for mcpx beats the id a host attaches automatically; a client that sends only the opencode key is keyed by it verbatim, exactly as it would be by dev.mcpx/session. The setting is hot: a change applies to the next request, not the next daemon start. Send a key outside the list and it is ignored, which is the documented fallback rather than an error.

What it touches

Identity reaches every backend path that picks an instance: mcpx_call, mcpx_exec (its artifact session), resources/read, prompts/get, completion/complete, the artifact listing in resources/list, and the ask path (/v1/ask with the same context). A caller with no identity lists no artifacts, because the daemon reads an empty session as “everyone’s”.

The tests are 2025-11-25/transport/http-mcp-clients-do-not-share-scoped-instances, 2026-07-28/transport/client-named-session-is-kept-across-requests, 2026-07-28/transport/unnamed-requests-are-their-own-scope (e2e, against a session-scoped fakemcp), and the TestEachConnectionHasItsOwnIdentity cases in internal/mcpserver/wp11_test.go.

What mcpx sends, per revision

mcpx serves five revisions from one server: 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 through an initialize handshake, and 2026-07-28 through per-request _meta. Everything is built in the newest shape and rewritten at one edge (downgrade in internal/mcpserver/revisions.go), because building several shapes and choosing is how they drift apart. This page records what that edge does and what was wrong before it did.

The modern envelope

Every 2026-07-28 result gets resultType (MUST), and _meta["io.modelcontextprotocol/serverInfo"] (SHOULD), merged into whatever _meta the result already carries. server/discover, the four list methods and resources/read also get ttlMs and cacheScope, which the caching page makes a MUST on complete results – and only complete ones: an input_required or a task handle carries none.

methodcacheScopettlMs from
server/discover, tools/list, prompts/list, resources/templates/listpublicprotoMessages.listMaxAge
resources/listprivate (it includes the caller’s session artifacts)protoMessages.listMaxAge
resources/readprivateprotoMessages.readMaxAge (0: a resource is whatever upstream says now)

Legacy clients get none of it; downgrade strips all four fields.

server/discover was answered with protocolVersions and a top-level serverInfo – neither is a DiscoverResult field – so no real modern client could read it. It is supportedVersions now, with the identity in _meta.

A modern request missing io.modelcontextprotocol/clientCapabilities, or naming a log level that does not exist, is -32602. mcpx emits no notifications/message at all, so the rule that none go to a request without a level holds trivially; a test pins it.

Per-revision fields

Checked against each revision’s schema.ts, for everything mcpx sends:

fieldfirst revisionolder clients get
audio content2025-03-26a text block describing it
Tool.annotations, completions capability2025-03-26nothing
title (tools, prompts, arguments, resources), resource_link, structuredContent, outputSchema2025-06-18nothing; a link becomes an embedded resource, structured content becomes text
icons, execution, core tasks capability, url elicitation2025-11-25nothing
extensions capability, resultType, cache hints, serverInfo in _meta2026-07-28nothing

Two mistakes this found. extensions was sent to 2025-11-25, whose ServerCapabilities has no such field. And resources/templates/list sent each template as {uri: ...}; every revision’s ResourceTemplate spells it uriTemplate, so the list was invalid everywhere.

2024-11-05 is served over stdio and Streamable HTTP. Its own HTTP+SSE transport is not: 2025-03-26 replaced it and 2026-07-28 says new implementations SHOULD NOT adopt it.

Serving 2024-11-05 does not make it the default for a message that names no revision on the transport. A Streamable HTTP request with no MCP-Protocol-Version header and no session, and a stdio batch before initialize, are taken as 2025-03-26 (Headerless): 2024-11-05 has no Streamable HTTP, so nothing on /mcp can be a 2024-11-05 request, the 2025-06-18/2025-11-25 transport pages say a server SHOULD assume 2025-03-26 there (2026-07-28: MAY), and a batch can only come from a client that believes batches exist – 2024-11-05’s schema has no batch type, 2025-03-26 says a server MUST accept them. Oldest still governs how a result is spelled for a stdio client that declared nothing, where guessing downward is the safe direction.

initialize

Every legacy lifecycle page: if the server does not support the requested version it MUST respond with another it supports. mcpx answered -32022 instead – a 2026-07-28 code a legacy client cannot interpret, and no version to fall back to. An unknown or modern version now gets the latest legacy one mcpx serves, and the client decides.

subscriptions/listen

The acknowledgement comes first and carries the listen request id in _meta["io.modelcontextprotocol/subscriptionId"] with the agreed filter; every notification on the stream carries the same key. A connection holds as many streams as the client opens, keyed by request id – mcpx kept one and replaced it on each listen. A notification kind no filter field names is dropped on the way out, whatever the notifier produced.

Over Streamable HTTP the listen POST’s response is the stream: an SSE response held open, with a comment every transport.sseKeepAlive (one keep-alive for every stream mcpx serves), until the client closes it (the transport’s cancellation). mcpx used to return at once, ending the response with only the acknowledgement on it. On stdio the client ends a stream with notifications/cancelled naming the listen id.

When mcpx ends a stream itself – the connection is going away, or the event source stopped – the specification contradicts itself. The subscriptions page says the server SHOULD send a successful subscriptions/listen result; the cancellation page says it MUST send notifications/cancelled for the listen id and MUST NOT send that notification for anything else. mcpx sends the cancellation first, while the request is still in progress, then the result, which a client that acted on the cancellation is told to ignore.

Legacy resources/subscribe keeps its own untagged stream.

Resource not found

-32602 to a 2026-07-28 client, -32002 to every earlier one, both with data.uri. The backend reports not-found as mcpserver.ErrResourceNotFound for four causes: not an mcpx:// URI, no namespace in it, a namespace that is not configured, or an upstream server that said -32002/-32602. Anything else is -32603; every failure used to be -32602, blaming the client’s URI for an upstream timeout. The upstream cases are recognised by the daemon’s error text, because the daemon returns them as text.

Error codes

mcpx’s server emits -32700, -32600..-32603, -32021 (a listen asking for taskIds without the tasks extension), -32022 (modern, unsupported version), and -32002 to legacy clients only. Nothing else in the reserved range; a test sweeps both eras.

Tasks

2025-11-25 core tasks are client-directed: a task field on tools/call, {task} back, tasks/list and a blocking tasks/result. Declared only to 2025-11-25; honoured from any legacy client.

The 2026-07-28 extension is server-directed, and forbids the other way: a server MUST ignore the task field and MUST NOT return a task to a client that did not declare io.modelcontextprotocol/tasks on that request. Whether a call becomes a task follows the tool’s execution.taskSupport, which a pass-through upstream declares and mcpx forwards on tools/list:

  • required, from a client that did not declare the extension: -32021 with data.requiredCapabilities.extensions["io.modelcontextprotocol/tasks"], before anything runs.
  • optional or required: the call gets 250 ms (Timing.TaskEager) to finish or to ask its first question. Finished, it is answered directly; a question is put to the client inline (input_required on the request, no task yet), and the retry carrying the answer goes through the same window. Past it, the call is a task. A question the call asks after that parks the task in input_required, with the question in inputRequests on tasks/get; tasks/update answers it key by key – answered keys leave inputRequests at once, keys not outstanding are ignored – and the task goes back to working. tasks/cancel abandons the call behind it.
  • anything else: run in line, and answered with a task only if it has not finished within protoMessages.taskAfter. A client that can answer questions inline is never handed one of these.
  • forbidden: never a task.

The CreateTaskResult is flat (resultType: "task", ttlMs, pollIntervalMs). tasks/get inlines the result or error; a tool result with isError is completed, not failed (the store’s 2025-11-25 rule is the opposite and is translated), and an upstream’s JSON-RPC error is failed with that error. tasks/update and tasks/cancel acknowledge with an empty result, tasks/cancel also on a task already finished. Mcp-Name on tasks/get, tasks/update and tasks/cancel mirrors the taskId; one that contradicts it is -32020, one left out is tolerated. tasks/list and tasks/result are -32601 to a modern client: the SEP says so, which overrides accepting liberally.

A task started on a legacy connection with an identity belongs to it: another connection cannot list it, and gets not-found for its id. Before, one server-wide store meant any HTTP client could list every other client’s tasks and read their results.

Elicitation

A 2026-07-28 client is never sent elicitationId; it was relayed verbatim inside inputRequests. A 2025-11-25 url-mode request REQUIRES one, so a question relayed from an upstream that did not supply it gets mcpx-<id>.

requestState

Bound to the request that carries it – its method and a SHA-256 of its parameters without _meta, inputResponses and requestState – plus expiry, under the per-process HMAC key. It was bound to an Mcp-Session-Id, which 2026-07-28 does not have: mcpx minted a session on server/discover to have something to bind to, and a client that skipped discover could not resume at all. That minting is gone. mcpx has no authenticated principal to add, so a state is replayable by anyone who holds it on the same request, within its TTL; the MRTR page notes that binding does not by itself make a state single-use.

tools/list order

The fixed tools come first in their written order; contributed tools – adapters, OpenAPI operations, /v1 operations – are sorted by name once, when they are added, so the list is the same across calls and restarts.

The official conformance suite, run against mcpx

created:      2026-09-30T12:00:00-05:00
last-updated: 2026-10-01T19:30:00-05:00
increment:    4
status:       standard
tags:         area:protocol, area:spec
description:  what modelcontextprotocol/conformance says about mcpx as a
              server and as a client, measured against the suite's own
              fixture server behind mcpx in pass-through mode, and what each
              remaining failure is.

In CI

.github/workflows/mcpx-conformance.yml runs every leg on each pull request and push to main that touches mcpx – one job per leg, about a minute each. A leg is one side of the protocol (mcpx as a server, or as a client) checked against one spec revision. The suite has frozen requirement sets for 2025-11-25 and 2026-07-28; for 2025-03-26 and 2025-06-18 it has only the scenarios tagged for each, selected with --spec-version, so those legs are smaller (about 3 and 56 checks). server-all is every server scenario it has. The suite does not know 2024-11-05, although mcpx carries its schema. Client legs run for all four: frozen sets for the two newest, tagged scenarios (--suite all --spec-version) for the two older. A leg’s job fails when any check fails, lists the failures on the run’s summary page, and uploads them as an artifact. It is not a merge block: no branch protection requires it.

A spec parity job compares the revisions mcpx supports (mcpx protocol --json, .asServer.supported) with those the suite knows (its error for an unknown --spec-version names them all). Neither list is written in this repository. A mismatch warns, with a deadline counted from the last match on main – 840 hours when only one side has extra revisions, 360 when both do – and after it the job fails. Revisions one side is expected to have alone are listed at the top of the workflow (SPEC_PARITY_ALLOW_ONLY_MCPX, SPEC_PARITY_ALLOW_ONLY_SUITE); today that is 2024-11-05, which mcpx serves and the suite has never tested. An entry both sides come to have, or that its side drops, fails the job at once until it is removed. See scripts/spec-parity.py.

Each leg’s summary shows every status the suite recorded: passed, failed, skipped, warning and info. A skipped check passes nothing, so the records job fails any run with more skipped checks than the record on main, and the record’s skip count only moves down. The table below lists every skip still standing, by name; --suite all skipped 7 of 280 checks at 650bf19 and 1 of 280 after resources/directory/read landed (LEGS=server-all scripts/conformance.sh, counted from checks.json). The Go tests have the same rule, through the repository variable MCPX_GO_TESTS written by scripts/gotest-report.py. The record is 0: every gap whose test was skipped has been closed, and the tests that only mean anything on macOS are compiled only there rather than counted as skipped on Linux. Both kinds of record keep the names of what is skipped, so a rise reports exactly which test or check is new.

Skipped, and why

The aim is zero failed and zero skipped. Every skip still standing is listed here with its reason; anything not listed is a regression the records catch.

wherewhatwhy it stays
client 2026-07-28http-standard-headers for initialize and notifications/initialized (2)A 2026-07-28 client never sends either: the revision removed the handshake. The suite checks the header on every method it knows and has no way to mark these not applicable. Permanent.
server 2026-07-28, --suite alltasks-status-notifications (1 each)The suite skips it unconditionally: src/scenarios/server/tasks/notifications.ts returns SKIPPED with “pending subscriptions/listen rewrite”. mcpx does deliver task status on a subscriptions/listen stream; TestListenDeliversTaskStatusNotifications covers it. Until the suite is rewritten.

Skipped checks: 4 of 1,014, all above. Skipped Go tests: 0.

Exceptions that were skips and are not any more: every conformance gap (#200, #201, #203, #209, #212, #77) whose test was skipped while the gap stood, all 42 skills checks (the last six, sep-2640-skills-directory, since mcpx serves resources/directory/read and mcpx-basics has an examples/ subfolder for the subdirectory check to find), the 2026-07-28 header checks for resources/read and prompts/get (the conformance adapter now reads every resource and gets every prompt in that scenario); one Go subtest for a method that was never applicable (initialize on a legacy connection, now not created); the spec example the schema rejects (now asserted rejected, so the day the spec fixes it the test fails and says to remove the entry); and two macOS-only filesystem tests, now compiled only on macOS rather than counted as skipped on Linux.

Records. Each leg keeps two records in a repository variable, each always one whole run. best is the lowest share of checks that did not pass – failed plus skipped – at a total no smaller than the record’s; a tie goes to the run with more checks passed. largest is the run with the most checks. Skips count against a run because a skipped check passes nothing: when only failures counted, a run that cut skips from 44 to 1 at zero failures tied the record and replaced nothing.

Each leg’s total is every check the suite recorded, from its checks.json files, warnings included: the summary line’s passed + failed leaves warnings out, so a failure that became a warning looked like a smaller run.

On main each leg’s counts are compared with two records kept in the repository variable MCPX_CONFORMANCE_<LEG> – the best failed/total ratio and the largest total – and a run that goes backwards on either gets a large warning on its summary. The rules are in scripts/conformance-record.py. Saving the records needs the secret MCPX_CONFORMANCE_VARS_TOKEN, a fine-grained token with Variables: read and write on this repository; GITHUB_TOKEN cannot write variables.

modelcontextprotocol/conformance (npm @modelcontextprotocol/conformance) is the suite the specification’s own process leans on: SEP-2484 makes a merged scenario a condition of a Standards Track SEP reaching Final, and SEP-1730 grades the official SDKs by it. Its requirements/<revision>.yaml sets are frozen, so a number from one of them means the same thing next year. The 2026-07-28 scenarios are on the alpha line (0.2.0-alpha.11, suite commit 7169291); latest is a 0.1.x that predates that revision.

scripts/conformance.sh runs it:

CONFORMANCE_DIR=/path/to/conformance scripts/conformance.sh [out-dir]
  • Server leg. The suite’s own reference fixture, examples/servers/typescript/everything-server.ts in the clone, started the way its package.json starts it (tsx everything-server.ts, PORT set), sits behind mcpx daemon --port … --passthrough demo. Pass-through (mcp.passthrough, ../protocol.md §2.5) offers the fixture’s tools, prompts and resources under their own names, so a scenario asking for test_simple_text or test://static-text gets them through mcpx and its checks reach mcpx’s protocol handling. The 2025-11-25 leg has its own daemon that talks 2025-11-25 to the fixture as well: the fixture’s legacy tools push requests to their client, which only a legacy session carries. --suite all and 2026-07-28 use a daemon that talks 2026-07-28 upstream. The suite has no stdio server mode, so mcpx serve is not covered here; schema_sweep_test.go covers it instead.
  • Client leg. internal/conformance/officialclient writes an isolated config naming the scenario server as an HTTP upstream and drives the real mcpx binary against it, so everything the scenario sees on the wire came from mcpx’s own client.

Every daemon and the fixture are stopped on exit.

Where the tasks fixtures come from. The tasks-extension scenarios (src/scenarios/server/tasks/*.ts) ask for greet, slow_compute, failing_job, protocol_error_job, confirm_delete, multi_input and test_tool_with_task. Nothing in the suite defines them: everything-server.ts does not, and the suite’s known-sdks.ts points each SDK at its own conformance server (test/conformance/src/everythingServer.ts in typescript-sdk, conformance/everything-server in go-sdk). They are SDK fixtures.

mcpx has its own: internal/testsupport/taskmcp, a stdio server defining exactly those tools with the behaviour each scenario’s “Required server fixtures” block documents. The daemon fronts it next to everything-server.ts with --passthrough demo,tasks, so one /mcp offers both surfaces under their own names (mcp.passthrough takes a list and refuses a name two upstreams share). taskmcp speaks 2025-11-25 and knows nothing of tasks: each tool declares execution.taskSupport and does its work – sleeps, fails, asks – and whether a call becomes a task, and how its questions travel, is mcpx’s doing. Two of its behaviours are chosen for that reason: confirm_delete, multi_input and protocol_error_job work for a second before they ask or fail, so the call has become a task by then; test_tool_with_task asks at once, so the question is answered inline before any task exists. That is the distinction the scenarios draw, seen from a gateway that cannot see into the tool.

taskmcp also lists echo_region, whose string parameter region carries x-mcp-header: "Region", because http-custom-header-server-validation reports all of its checks as untestable without such a tool. mcpx relists it unchanged and validates the Mcp-Param-Region header itself, before the call is forwarded.

Results

Every column is a run of scripts/conformance.sh against a daemon built from that commit. Raw output is per leg in the out-dir (<leg>/out.txt, <leg>/results/**/checks.json), and failures.txt has one line per failed check. Columns before 64f3471 put mcpx’s test fake (one tool, echo) behind the daemon; 64f3471 is that setup re-measured as the “before” of this change; the last column is the fixture in pass-through.

leg05c78b2 (first run)408bc2b60068c664f3471 (fake)mcpx-fix2 (fixtures, pass-through)
server --spec-version 2025-03-26not runnot runnot runnot run3 passed, 0 failed
server --spec-version 2025-06-18not runnot runnot runnot run56 / 0
server --requirements 2025-11-2547 passed, 19 failed45 / 2145 / 2145 / 2180 / 0
server --requirements 2026-07-2860 / 104117 / 54109 / 62110 / 62195 / 0
server --suite all84 / 106136 / 61128 / 67131 / 67229 / 0
client --spec-version 2025-03-26not runnot runnot runnot run0 / 7 (both scenarios are OAuth)
client --spec-version 2025-06-18not runnot runnot runnot run3 / 9
client --requirements 2025-11-255 / 6420 / 5620 / 4820 / 5620 / 56
client --requirements 2026-07-2823 / 8462–63 / 68–6963 / 5963 / 6863 / 68

No server check fails, in any leg. The --suite all leg needed one thing beyond the fixtures: its daemon fronts the upstream with protocol: follow, so a caller on a pre-2026 revision gets a legacy upstream session and the fixture’s server-to-client requests (elicitation, sampling) reach it. Under the default prefer-discover those four scenarios fail, because a 2026-07-28 upstream session cannot carry a request to its client. See docs/spec/era-probe.md.

Every client failure is an OAuth scenario under the suite’s auth/; mcpx has no OAuth client (#253). The workflow holds each client leg to exactly these counts with nothing failing outside auth/.

Totals moved by more than the failures did, because the fixture makes the suite run checks it skipped before (a scenario stops at its first missing fixture). Read the failure lists, not the totals.

What running against a real fixture found in mcpx

Each is fixed, with a test that fails without the fix:

  • An upstream’s question never reached the client once two calls shared an instance. A question raised by a 2026-07-28 upstream was attributed to the waiting call by (server, instance) alone; with a second call in flight on the instance – one earlier client that never resumed was enough – it was left to the broker, and the request hung until the suite’s timeout (“This operation was aborted”, 12 input-required-result-* checks). It is now attributed through the call’s own context (internal/daemon/routes_proto.go askFor; TestAQuestionOnACallsContextIsThatCalls).
  • Several questions in one round reached the client one round at a time, renamed to mcpx’s ids. An upstream’s inputRequests are now answered concurrently and relayed together under the upstream’s own keys (mcpclient.resolveInput, mcpserver.wireKey; TestInputRequestsOfOneRoundAreAskedTogetherUnderTheirKeys, TestInputRequestKeysKeepTheUpstreamsNames).
  • Two rounds of one call got the same requestState when minted within a second (sep-2322-multi-round-r2); the state now carries a nonce (TestRequestStateDiffersPerRound).
  • resources/list carried every resource template as a resource with an empty URI (resources-list: “Resource 0: missing uri”); before pass-through it read mcpx://<ns>/, which no check caught (TestResourcesListOmitsTemplates).
  • A template’s {variables} were percent-encoded by the mcpx:// rewrite, turning …/{id} into a fixed URI (TestResourcesListOmitsTemplates).
  • resources/templates/list on a cold daemon listed no server’s templates: it read the schema cache without filling it, as resources/list does (TestTemplatesListedColdIncludeTheServers).
  • A resource read answered on the ask path came back as mcpx://mcpx://<server>//<uri> (TestAskPathResourceURIsAreNamespacedOnce).

And the pass-through mode itself: TestPassthroughExposesOneUpstreamUnrenamed, TestPassthroughToolQuestionsReachTheClient.

Unknown tool is -32602 (#284) on every revision mcpx serves: TestUnknownToolIsAProtocolError (in-process, 2025-11-25 and 2026-07-28) and TestUnknownToolIsAProtocolErrorOverHTTP (over /mcp, each of 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 through its own session, and 2026-07-28), both in internal/mcpserver, which CI’s go test ./... runs.

Client-leg flake, fixed in the adapter

elicitation-sep1034-client-defaults passed alone and failed in the parallel --requirements run: the adapter looked for pending questions by running mcpx elicit list every 200 ms, a process start per tick, and under load a tick outlived the scenario. It now reads the broker’s store in process every 20 ms and answers there (officialclient answerElicitations). Three consecutive client runs: 20/56 and 63/68 each time, every non-auth/* scenario passing in all three.

What the remaining failures are

Server, 2025-11-25 (3)

scenario / checkmessagekind
tools-call-with-loggingNo log notifications receivedmcpx defect: no relay of upstream notifications/message (#212)
tools-call-with-progressNo progress notifications receivedmcpx defect: no relay of upstream notifications/progress (#212)
server-sse-polling / scenario-timeoutdid not complete within 30000msopen: the fixture closes the POST stream to mcpx (SEP-1699); mcpx reconnects with Last-Event-ID (the fixture logs it) and no replayed response arrives, so the call never finishes. Not scored for 2025-11-25 (pending); root cause not established

Server, 2026-07-28 (12 failed, 2 warnings, with taskmcp fronted)

Every tasks-extension check passes. Before taskmcp, 27 of them failed as no tool named "…" or as untestable downstream of one.

scenario / checkmessagekind
tools-call-with-progressNo progress notifications receivedmcpx defect (#212)
server-stateless / sep-2575-http-server-no-independent-requests-on-stream, sep-2575-server-no-log-without-loglevelno frames from the streaming / logging toolmcpx defect: same relay gap (#212)
server-stateless / sep-2575-server-sends-prompts-list-changed-on-subscription (WARNING)no notifications/prompts/list_changed on the listen streammcpx gap: an upstream’s list change on its listen stream is not subscribed to and relayed
server-stateless / sep-2575-server-rejects-undeclared-capability, sep-2575-missing-capability-http-400executed test_missing_capability although the client did not declare samplingmcpx defect: the upstream sees mcpx’s own client capabilities, not the calling client’s; a pass-through call should carry the caller’s
input-required-result-basic-list-roots / sep-2322-list-roots-incomplete; input-required-result-multiple-input-requests / sep-2322-multiple-inputs-incomplete (2 of 3 asked)no roots/list inputRequestdecision to revisit: mcpx answers an upstream’s roots/list from its own configured roots and never relays it
input-required-result-ignore-extra-params / sep-2322-ignore-unexpected-params (WARNING)no complete resultmcpx gap: a request carrying inputResponses without a requestState is treated as a new call; the fixture accepts such answers directly, mcpx does not forward them

Server, --suite all (17)

The 2026-07-28 list, plus five legacy-tool checks that only the 2025-11-25 daemon can pass: tools-call-elicitation, tools-call-sampling, elicitation-sep1034-defaults, elicitation-sep1330-enums (“Server did not request … from client”) and tools-call-with-logging. --suite all runs against the 2026-07-28 daemon, and over 2026-07-28 the fixture itself answers its legacy tools’ pushed requests -32601.

Client, both sets

Every remaining failure is auth/*: mcpx has no OAuth client (#253), and its per-server auth block is parsed and never applied (#240).

What the suite does not see

  • mcpx serve over stdio: no stdio server mode in the suite.
  • Legacy revisions other than 2025-11-25: there are no requirement sets for them. internal/mcpserver/requirements_test.go and the schema sweep cover 2024-11-05 through 2025-06-18.

The per-requirement status, with the test that verifies each row, is conformance-matrix.md.

MCP normative requirements, every revision, both sides

WP6 output 1. Merged from five area sweeps (wp6-raw/A..E.md, which keep the per-area source lists, conventions and implied-details notes). Sources: prose of every page under docs/specification/<rev>/, schema/<rev>/schema.ts doc comments, docs/extensions/tasks/overview.mdx.

Columns: id | revs (a range a..b when unchanged) | side | level | area | quote | URL | applies to mcpx as server | as client | changes (non-empty = the obligation differs in another revision or is contradicted elsewhere; the full list is revision-conflicts.md).

Conventions carried from the sweeps: level (schema) / (implied) / (lowercase) / (changelog) qualifiers mark where the keyword came from; side also uses auth-server and intermediary where the spec addresses them. The two mcpx columns are a reviewer’s notes, not a status: the status of each requirement is conformance-matrix.md, computed from the tests. A note containing the word gap (any case) claims mcpx does not meet the requirement; TestMatrix (internal/conformance) fails when a cell carrying that claim has a covering test, so a note cannot keep claiming a gap after the fix lands, nor a test pass while a note says it should not. Anchors are heading slugs derived by hand, not checked against the live site. 2024-11-05 and 2025-03-26 have no schema page; schema-only rows for them link GitHub schema.ts.

Counts

788 rows (815 before de-duplication; 27 cross-area duplicates folded, listed at the end). 418 rows carry a changes note.

A row counts in every revision its range covers.

revisiontotalauth-serverbothclientintermediaryserver
2024-11-0518306359061
2025-03-2625308580088
2025-06-18292293950102
2025-11-2546431661400155
2026-07-2855631231833244

Most 2025-11-25 growth is the tasks page (57 rows) and the elicitation rewrite; most 2026-07-28 growth is discover, versioning, MRTR, subscriptions, caching and the transport rewrite.

Lifecycle, versioning, messages, errors, _meta, JSON Schema, ping

idrevssidelevelareaquoteurlmcpx-as-servermcpx-as-clientchanges
messages-all-messages-follow-jsonrpc-22024-11-05..2026-07-28bothMUSTmessages“All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#messagesyes, except batches (see batch row) and parse-error id (see error-id row)yes2024-11-05 also states it on basic/messages (“All messages in MCP MUST follow…”). “Follow JSON-RPC 2.0” imports batch support and id: null on parse errors, which MCP later narrows — see Conflicts C1, C4
lifecycle-implementations-support-base-and-lifecycle2024-11-05..2025-11-25bothMUSTlifecycle“All implementations MUST support the base protocol and lifecycle management components.”https://modelcontextprotocol.io/specification/2025-11-25/basic/indexyesyesReplaced in 2026-07-28 by next row
versioning-implementations-support-base-versioning-patterns2026-07-28bothMUSTversioning“All implementations MUST support the base protocol, versioning, and the message patterns.”https://modelcontextprotocol.io/specification/2026-07-28/basic/indexpartial: request/response and MRTR yes; subscriptions/listen yes; versioning partial (discover shape wrong, see versioning rows)partial: modern path exists; does not retry on -32022Lifecycle requirement dropped; “message patterns” includes MRTR and subscribe/notify
messages-other-components-may2024-11-05..2026-07-28bothMAYmessages“Other components MAY be implemented based on the specific needs of the application.”https://modelcontextprotocol.io/specification/2026-07-28/basic/indexyesyes
messages-request-id-string-or-integer2024-11-05..2026-07-28bothMUSTmessages“Requests MUST include a string or integer ID.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requestsyes (server-initiated ids are negative integers)yes (atomic int counter)
messages-request-id-not-null2024-11-05..2026-07-28bothMUST NOTmessages“Unlike base JSON-RPC, the ID MUST NOT be null.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requestsyesyes
messages-request-id-unique-per-session2024-11-05..2025-11-25bothMUST NOTmessages“The request ID MUST NOT have been previously used by the requestor within the same session.”https://modelcontextprotocol.io/specification/2025-11-25/basic/index#requestsyes (monotonic counter per Conn)yes (monotonic counter per Client)Relaxed in 2026-07-28 (next row). Conflict C2
messages-request-id-unique-among-in-flight2026-07-28bothMUST NOTmessages“The request ID MUST NOT match the ID of any other request the sender has issued and not yet received a response for.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requestsn/a (modern server sends no requests)yesReuse after response becomes legal; changelog item 9 still says re-issue “with a new request ID” — Conflict C2
messages-requests-client-to-server-only2026-07-28both(definitional)messages“Requests are sent from the client to the server, to initiate an operation.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requestsn/a on modern path; on legacy path mcpx still sends elicitation/sampling requests (allowed there)yes: answers ping/roots/elicitation/sampling requests from legacy servers2024-11-05..2025-11-25: “from the client to the server or vice versa”. Normative MUST NOT is in patterns row below
messages-servers-must-not-initiate-requests2026-07-28serverMUST NOTmessages“Servers MUST NOT initiate JSON-RPC requests, and clients do not send JSON-RPC responses.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/indexyes on modern requests (input_required instead); legacy connections still get wire requestsn/aNew. Legal-then-forbidden: every legacy server-initiated request (ping, sampling, roots, elicitation). Conflict C3
messages-response-same-id2024-11-05..2025-06-18bothMUSTmessages“Responses MUST include the same ID as the request they correspond to.”https://modelcontextprotocol.io/specification/2025-06-18/basic/index#responsesyes for requests; no for parse errors (id omitted)yesSplit in 2025-11-25 into result/error rows; error ID exception added. Conflict C4
messages-response-result-xor-error2024-11-05..2025-06-18bothMUST / MUST NOTmessages“Either a result or an error MUST be set. A response MUST NOT set both.”https://modelcontextprotocol.io/specification/2025-06-18/basic/index#responsesyes (Result is omitempty: a nil result would violate; not observed on handled paths)yesRe-expressed as two shapes in 2025-11-25
messages-result-response-same-id2025-11-25..2026-07-28bothMUSTmessages“Result responses MUST include the same ID as the request they correspond to.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responsesyesyes
messages-result-response-has-result2025-11-25..2026-07-28bothMUSTmessages“Result responses MUST include a result field.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responsesyesyes
messages-result-any-object2025-03-26..2026-07-28bothMAYmessages“The result MAY follow any JSON object structure.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responsesyesyes2024-11-05: lowercase “Results can follow any JSON object structure” (basic/index)
messages-error-response-same-id-unless-unreadable2025-11-25..2026-07-28bothMUSTerrors“Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request).”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-responsesyes (parse error omits id — permitted here by schema id?)yesSchema JSONRPCErrorResponse.id became optional in 2025-11-25; before, JSONRPCError.id: RequestId was required. Conflict C4
errors-error-has-code-and-message2024-11-05..2026-07-28bothMUSTerrors“Error responses MUST include an error field with a code and message.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-responsesyesyes2024-11-05 lowercase “errors must include an error code and message at minimum”; 2025-03-26..2025-06-18 “errors MUST include an error code and message at minimum”
errors-codes-integers2024-11-05..2026-07-28bothMUSTerrors“Error codes MUST be integers.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-responsesyesyes
errors-message-concise-single-sentence2024-11-05..2026-07-28bothSHOULDerrors“The message SHOULD be limited to a concise single sentence.” (schema)https://modelcontextprotocol.io/specification/2026-07-28/schema#errorpartly: some messages embed Go error stringsn/a (sends no errors except to server requests)
errors-data-may2026-07-28bothMAYerrors“Error responses MAY include a data member with additional information of any type”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-responsesyesn/aEarlier revisions: schema data?: unknown, no prose
errors-standard-jsonrpc-codes2026-07-28both(definitional)errors“MCP uses the standard JSON-RPC 2.0 error codes (-32700, -32600 to -32603) for general protocol failures.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyesn/a
errors-legacy-subrange-no-new-codes2026-07-28bothMUST NOT / SHOULD NOTerrors“New codes MUST NOT be allocated in this sub-range, and new implementations SHOULD NOT use codes from this sub-range at all.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyes (uses none in -32000..-32019)yesSchema comment calls the sub-range “implementation-defined”, not “legacy” — Conflict C10
errors-receivers-no-meaning-for-legacy-subrange2026-07-28bothMUST NOTerrors“Apart from -32002 (see below), receivers MUST NOT assume any specific meaning for these codes.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyesyes (treats as opaque upstream errors)Legacy SDK codes (e.g. -32001 request timeout in TS SDK) lose any shared meaning
errors-reserved-subrange-only-spec-codes2026-07-28bothMUST NOT / MUSTerrors“Implementations MUST NOT emit any code from this sub-range that is not defined by this specification and MUST use defined codes only with their specified meanings.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyes (-32022 only, and with its meaning; also emits -32022 to legacy initialize — see negotiation rows)n/a2025-11-25 used -32042 (URL elicitation) in this sub-range; 2026 draft history renumbered -32001/-32003/-32004 → -32020/-32021/-32022
errors-do-not-emit-32002-and-320422026-07-28bothMUST NOTerrors“Implementations of this protocol version MUST NOT emit these codes:” (-32002, -32042)https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyes (mcpx emits neither)n/aBoth were legal (and -32002 the specified code) in earlier revisions. Conflict C11
errors-client-accept-32002-from-legacy2026-07-28clientSHOULDerrors“Clients SHOULD still accept -32002 from servers implementing earlier versions.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesn/ayes (passes upstream errors through)
errors-local-errors-not-mistaken-for-peer2026-07-28bothshould (lowercase)errors“Implementations surfacing local errors in JSON-RPC-shaped structures should ensure they cannot be mistaken for errors received from the peer.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codespartly: proxy re-emits upstream failures as its own JSON-RPC errors / isError results; origin is text-onlyn/aNot RFC 2119 (lowercase) but matters for a proxy: mcpx is exactly the case of relaying local vs peer errors
errors-new-codes-outside-jsonrpc-range2026-07-28bothSHOULDerrors“New error codes for purposes not defined by this specification SHOULD be allocated outside the JSON-RPC reserved range (-32768 to -32000)”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyes (defines none)n/a
errors-method-not-found-for-unadvertised-capability2026-07-28server(schema guidance)errors“a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise” (schema)https://modelcontextprotocol.io/specification/2026-07-28/schema#methodnotfounderrorno: mcpx answers methods it did not advertise for that revision (accept-liberally, e.g. resources/subscribe from modern)n/aDirectly opposes mcpx’s “accept liberally” rule for gated methods; wording is descriptive, not RFC keyword
messages-notification-no-response2025-03-26..2026-07-28bothMUST NOTmessages“The receiver MUST NOT send a response.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#notificationsyes (handle returns nil for notifications; HTTP 202)yesImplicit from JSON-RPC in 2024-11-05
messages-notification-no-id2024-11-05..2026-07-28bothMUST NOTmessages“Notifications MUST NOT include an ID.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#notificationsyesyes
messages-batch-may-send-must-receive2025-03-26bothMAY / MUSTmessages“MCP implementations MAY support sending JSON-RPC batches, but MUST support receiving JSON-RPC batches.”https://modelcontextprotocol.io/specification/2025-03-26/basic/index#batchingno: stdio server unmarshals each line into one request object; an array gets -32700yes on HTTP responses (mcpclient/http.go accepts a batch array); stdio not checkedRemoved in 2025-06-18 (changelog “Remove support for JSON-RPC batching”). Conflict C1
lifecycle-initialize-not-in-batch2025-03-26clientMUST NOTlifecycle“The initialize request MUST NOT be part of a JSON-RPC batch”https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#initializationn/ayes (never batches)Only meaningful in 2025-03-26
lifecycle-initialization-first-interaction2024-11-05..2025-11-25bothMUSTlifecycle“The initialization phase MUST be the first interaction between client and server.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationnot enforced: requests before/without initialize are served as 2025-03-26 (accept-liberally)yes (legacy mode sends initialize first)Removed in 2026-07-28 (no handshake); a dual-era server cannot enforce it because modern requests have no initialize
lifecycle-client-sends-initialize2024-11-05..2025-11-25clientMUSTlifecycle“The client MUST initiate this phase by sending an initialize request containing:” version, capabilities, implementation infohttps://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationn/ayesRemoved 2026-07-28
lifecycle-server-responds-caps-and-info2024-11-05..2025-11-25serverMUSTlifecycle“The server MUST respond with its own capabilities and information”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationyes (capabilities per negotiated revision, serverInfo, instructions)n/aRemoved 2026-07-28
lifecycle-client-sends-initialized-after-init2024-11-05..2025-11-25clientMUSTlifecycle“After successful initialization, the client MUST send an initialized notification”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationn/a (accepts notifications/initialized and bare initialized, does not require it)yesRemoved 2026-07-28
lifecycle-client-no-requests-before-init-response2024-11-05..2025-11-25clientSHOULD NOTlifecycle“The client SHOULD NOT send requests other than pings before the server has responded to the initialize request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationn/ayes2026 dual-era clients send server/discover before initialize as a probe — legal only because that probe precedes the decision to be legacy
lifecycle-server-no-requests-before-initialized2024-11-05..2025-11-25serverSHOULD NOTlifecycle“The server SHOULD NOT send requests other than pings and logging before receiving the initialized notification.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initializationyes (asks only inside a client call, which follows initialize)n/a“logging” is a notification, not a request — Conflict C8
lifecycle-client-sends-supported-version2024-11-05..2025-11-25clientMUSTversioning“In the initialize request, the client MUST send a protocol version it supports.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationn/ayes (2025-11-25)
lifecycle-client-sends-latest-version2024-11-05..2025-11-25clientSHOULDversioning“This SHOULD be the latest version supported by the client.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationn/ayes (latest legacy; 2026-07-28 cannot be sent over initialize)
lifecycle-client-may-support-older2024-11-05..2025-11-25clientMAYversioning“The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well.” (schema)https://modelcontextprotocol.io/specification/2025-11-25/schema#initializerequestparamsn/ayes (accepts any version back — see client-disconnect row)
lifecycle-server-echoes-supported-version2024-11-05..2025-11-25serverMUSTversioning“If the server supports the requested protocol version, it MUST respond with the same version.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationyes (legacy versions echoed)n/a
lifecycle-server-counteroffers-version2024-11-05..2025-11-25serverMUSTversioning“Otherwise, the server MUST respond with another protocol version it supports.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationno: negotiate() returns “” and mcpx answers -32022 with the legacy list (e.g. a 2024-11-05 client, or 2026-07-28 over initialize) instead of a result carrying another versionn/aThe page’s own example answers with an error (-32602) instead — Conflict C5. mcpx’s -32022 is a 2026-only code sent on a legacy exchange
lifecycle-server-counteroffers-latest2024-11-05..2025-11-25serverSHOULDversioning“This SHOULD be the latest version supported by the server.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationno (no counter-offer at all)n/a
lifecycle-client-disconnects-on-unsupported-version2024-11-05..2025-11-25clientSHOULD (prose) / MUST (schema)versioningprose: “If the client does not support the version in the server’s response, it SHOULD disconnect.” schema: “If the client cannot support this version, it MUST disconnect.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationn/ano: initializeLegacy records ir.ProtocolVersion as Negotiated without checking it (a 2024-11-05 or unknown answer is accepted)Prose vs schema — Conflict C6
lifecycle-http-protocol-version-header2025-06-18..2025-11-25clientMUSTversioning“If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiationn/anot verified here (transport owner)2026-07-28: header on every request, must match _meta (transport area)
lifecycle-capabilities-establish-session-features2024-11-05..2025-11-25both(definitional)lifecycle“Client and server capabilities establish which optional protocol features will be available during the session.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#capability-negotiationyes (per-Conn caps recorded at initialize)yes2026: per-request clientCapabilities; server caps via server/discover
lifecycle-respect-negotiated-version-and-capabilities-should2024-11-05..2025-03-26bothSHOULDlifecycle“Both parties SHOULD: Respect the negotiated protocol version; Only use capabilities that were successfully negotiated”https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#operationyes (downgrade(), capability gates)yesUpgraded to MUST in 2025-06-18 (changelog item 9)
lifecycle-respect-negotiated-version-and-capabilities-must2025-06-18..2025-11-25bothMUSTlifecycle“Both parties MUST: Respect the negotiated protocol version; Only use capabilities that were successfully negotiated”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#operationpartly: sends only negotiated shapes; accepts un-negotiated methods (tasks/*, subscriptions/listen) by design — receiving is not “using”partly: only uses server caps it saw; Complete gated on completionsConflict C7 (accept-liberally vs MUST). 2026: architecture “Both parties must respect declared capabilities throughout the interaction” (lowercase)
lifecycle-stdio-client-shutdown-sequence2024-11-05..2025-11-25clientSHOULDlifecycle“the client SHOULD initiate shutdown by: 1. First, closing the input stream to the child process … 2. … SIGTERM … 3. Sending SIGKILL”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#stdion/ayes (mcpclient/stdio.go closes stdin, waits with timeout, then kills) — ordering beyond that not verifiedMoved to transports/stdio in 2026-07-28 (transport area)
lifecycle-stdio-server-may-exit2024-11-05..2025-11-25serverMAYlifecycle“The server MAY initiate shutdown by closing its output stream to the client and exiting.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#stdioyes (exits on stdin EOF)yes (treats Recv error as connection failure)
lifecycle-http-shutdown-closing-connection2024-11-05..2025-11-25both(definitional)lifecycle“For HTTP transports, shutdown is indicated by closing the associated HTTP connection(s).”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#httpyes (+ DELETE drops session)yes
lifecycle-prepared-for-error-cases2024-11-05..2025-11-25bothSHOULDlifecycle“Implementations SHOULD be prepared to handle these error cases:” (version mismatch, capability failure, request timeouts)https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#error-handlingpartlypartly: version mismatch not handled (see disconnect row)2024-11-05 list: “Initialize request timeout”, “Shutdown timeout”; 2025-03-26+: “Request timeouts”
lifecycle-timeouts-all-requests-20242024-11-05bothSHOULDlifecycle“Implementations SHOULD implement appropriate timeouts for all requests, to prevent hung connections and resource exhaustion.”https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle#error-handlingn/a (2024-11-05 not served)yes (pool call timeout, HTTP client timeout)Replaced by the Timeouts section in 2025-03-26
lifecycle-timeouts-establish2025-03-26..2025-11-25bothSHOULDlifecycle“Implementations SHOULD establish timeouts for all sent requests, to prevent hung connections and resource exhaustion.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeoutsyes (proto.askTimeout on server-initiated requests)yes2026-07-28 moves the same text to basic/patterns/cancellation#timeouts (cancellation owner)
lifecycle-timeouts-cancel-on-expiry2025-03-26..2025-11-25bothSHOULDlifecycle“the sender SHOULD issue a cancellation notification for that request and stop waiting for a response.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeoutsnot verifiedno: mcpclient/http.go comment records cancellation was made best-effort then removed2026 wording: “the sender SHOULD cancel the request” (cancellation page)
lifecycle-timeouts-per-request-configurable2025-03-26..2025-11-25bothSHOULDlifecycle“SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis.”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeoutspartly (global settings)yes (CallTimeout)mcpx is “other middleware”
lifecycle-timeouts-may-reset-on-progress2025-03-26..2025-11-25bothMAYlifecycle“Implementations MAY choose to reset the timeout clock when receiving a progress notification”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeoutsn/anot verified
lifecycle-timeouts-max-regardless-of-progress2025-03-26..2025-11-25bothSHOULDlifecycle“implementations SHOULD always enforce a maximum timeout, regardless of progress notifications”https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeoutsyes (askTimeout, askRounds)yes
lifecycle-cancel-not-initialize2024-11-05..2025-11-25clientMUST NOTlifecycle“A client MUST NOT attempt to cancel its initialize request.” (schema)https://modelcontextprotocol.io/specification/2025-11-25/schema#cancellednotificationn/ayesGone in 2026 (no initialize)
lifecycle-instructions-may-go-in-system-prompt2024-11-05..2025-11-25clientMAYlifecycle“For example, this information MAY be added to the system prompt.” (schema, InitializeResult.instructions)https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresultyes (sends instructions)yes (stores Instructions)2026: DiscoverResult.instructions, no RFC keyword. Field appears in 2024-11-05 schema although the 2024-11-05 lifecycle example lacks it (example added 2025-03-26)
lifecycle-capability-sets-open2024-11-05..2026-07-28both(definitional)lifecycle“Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.” (schema)https://modelcontextprotocol.io/specification/2026-07-28/schema#clientcapabilitiesyes (tolerates unknown keys)yes2026 adds extensions map; tasks leaves core caps (2025-11-25 only)
lifecycle-implementation-name-version-required2024-11-05..2026-07-28bothREQUIRED (schema field)lifecyclename: string; version: string required on Implementationhttps://modelcontextprotocol.io/specification/2026-07-28/schema#implementationyesyestitle added 2025-06-18; icons, description, websiteUrl added 2025-11-25. mcpx sends none of the optional fields
lifecycle-include-context-only-if-declared2025-11-25..2026-07-28serverSHOULDlifecycle“If not declared, servers SHOULD only use includeContext: \"none\" (or omit it).” (schema, ClientCapabilities.sampling.context)https://modelcontextprotocol.io/specification/2026-07-28/schema#clientcapabilitiesyes (passes upstream sampling through; does not add includeContext)n/aSampling-area detail located in capability schema
ping-receiver-responds-promptly-empty2024-11-05..2025-11-25bothMUSTping“The receiver MUST respond promptly with an empty response”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#behavior-requirementsyes ({}; plus resultType if request had modern _meta)yes (answer() returns {})Removed in 2026-07-28 (changelog item 5; absent from schema). Conflict C3
ping-either-party-may-initiate2024-11-05..2025-11-25both(definitional)ping“Either the client or server can initiate a ping by sending a ping request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#overviewmcpx never pings its clientsClient.Ping exists but no periodic caller found
ping-sender-may-consider-stale2024-11-05..2025-11-25bothMAYping“If no response is received within a reasonable timeout period, the sender MAY: Consider the connection stale; Terminate the connection; Attempt reconnection procedures”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#behavior-requirementsn/an/a (does not ping)
ping-periodic-pings2024-11-05..2025-11-25bothSHOULDping“Implementations SHOULD periodically issue pings to detect connection health”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#implementation-considerationsno (not-implemented-by-choice: HTTP is per-request, stdio exits on EOF)no (not-implemented-by-choice; pool detects dead processes on use)Uncertain whether a pool health check uses ping — not found in grep
ping-frequency-configurable2024-11-05..2025-11-25bothSHOULDping“The frequency of pings SHOULD be configurable”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#implementation-considerationsn/an/a
ping-timeouts-appropriate2024-11-05..2025-11-25bothSHOULDping“Timeouts SHOULD be appropriate for the network environment”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#implementation-considerationsn/an/a
ping-avoid-excessive2024-11-05..2025-11-25bothSHOULDping“Excessive pinging SHOULD be avoided to reduce network overhead”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#implementation-considerationsyesyes
ping-timeout-is-connection-failure2024-11-05..2025-11-25bothSHOULDping“Timeouts SHOULD be treated as connection failures”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#error-handlingn/an/a
ping-multiple-failures-may-reset2024-11-05..2025-11-25bothMAYping“Multiple failed pings MAY trigger connection reset”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#error-handlingn/an/a
ping-log-failures2024-11-05..2025-11-25bothSHOULDping“Implementations SHOULD log ping failures for diagnostics”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#error-handlingn/an/a
ping-schema-receiver-must-respond2024-11-05..2025-11-25bothmust (lowercase)ping“The receiver must promptly respond, or else may be disconnected.” (schema)https://modelcontextprotocol.io/specification/2025-11-25/schema#pingrequestyesyes
meta-reserved-for-mcp2024-11-05..2025-03-26both(definitional)meta“This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses.” (schema)GH(2025-03-26) Result._metayesyesNo key-format rules before 2025-06-18; progressToken the only named key
meta-no-assumptions-on-reserved-keys2025-06-18..2026-07-28bothMUST NOTmeta“implementations MUST NOT make assumptions about values at these keys.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayesyes2025-06-18/2025-11-25: “MUST NOT” not bolded, same force
meta-prefix-label-format2025-06-18..2026-07-28bothMUSTmeta“If specified, MUST be a series of labels separated by dots (.), followed by a slash (/).” / “Labels MUST start with a letter and end with a letter or digit”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayes (only io.modelcontextprotocol/*)yes
meta-reserved-prefix-2025-06-182025-06-18both(reservation)meta“Any prefix beginning with zero or more valid labels, followed by modelcontextprotocol or mcp, followed by any valid label, is reserved for MCP use.”https://modelcontextprotocol.io/specification/2025-06-18/basic/index#metan/an/aReplaced 2025-11-25 by reverse-DNS rule; the two rules reserve different sets — Conflict C9
meta-reverse-dns-should2025-11-25..2026-07-28bothSHOULDmeta“Implementations SHOULD use reverse DNS notation (e.g., com.example/ rather than example.com/).”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayesyes
meta-reserved-prefix-second-label2025-11-25..2026-07-28both(reservation)meta“Any prefix where the second label is modelcontextprotocol or mcp is reserved for MCP use.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayesyesConflict C9
meta-name-format2025-06-18..2026-07-28bothMUST / MAYmeta“Unless empty, MUST begin and end with an alphanumeric character ([a-z0-9A-Z]).” / “MAY contain hyphens (-), underscores (_), dots (.), and alphanumerics in between.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayesyes
meta-schema-may-reserve-names2025-06-18..2026-07-28both(reservation)meta“definitions in the schema may reserve particular names for purpose-specific metadata”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayesyes2026 page drops this sentence from prose; retained in schema MetaObject comment
meta-progress-token-receiver-not-obligated2024-11-05..2026-07-28both(definitional)meta“The receiver is not obligated to provide these notifications.” (schema, _meta.progressToken)https://modelcontextprotocol.io/specification/2026-07-28/schema#requestmetaobjectyesyesUnprefixed reserved key — exception to prefix rule, never stated as such (only traceparent etc. are called an “exception”)
meta-request-params-meta-required2026-07-28clientMUST / REQUIREDmeta“fields marked as required MUST be included on every request.” (protocolVersion, clientCapabilities)https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metan/ayes (withMeta on every modern request)Schema makes RequestParams._meta non-optional, hence every request needs params — Conflict C12
meta-server-rejects-missing-required-field2026-07-28serverMUSTmeta“A request missing any required field is malformed; the server MUST reject it with JSON-RPC error code -32602 (Invalid params). On HTTP, the response status MUST be 400 Bad Request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metano (not-implemented-by-choice: a request with no _meta is treated as legacy / 2025-03-26, which a dual-era server has to do)n/aConflicts with versioning matrix “exact code is implementation-defined” for initialize — Conflict C13
meta-client-should-send-clientinfo2026-07-28clientSHOULDmeta“Clients SHOULD include io.modelcontextprotocol/clientInfo on every request unless specifically configured not to do so.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metan/ayes
meta-server-no-undeclared-client-capabilities2026-07-28serverMUST NOTmeta“A server MUST NOT rely on capabilities the client has not declared.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayes (Peer.CanElicit/CanSample read per-request caps)n/aLegacy analogue is lifecycle “Only use capabilities that were successfully negotiated”
meta-server-no-infer-caps-from-prior2026-07-28serverMUST NOTmeta“Servers MUST NOT infer capabilities from prior requests.” (schema)https://modelcontextprotocol.io/specification/2026-07-28/schema#requestmetaobjectyes for modern requests; a request without _meta on a Conn that initialized falls back to Conn caps (legacy semantics)n/a
errors-missing-required-client-capability2026-07-28serverMUSTerrors“the server MUST return a MissingRequiredClientCapabilityError (-32021) whose data.requiredCapabilities lists the missing capabilities. On HTTP, the response status MUST be 400 Bad Request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metan/a in practice: mcpx never requires a client capability; questions a client cannot answer go to the brokern/a
meta-server-should-send-serverinfo2026-07-28serverSHOULDmeta“Servers SHOULD include the following io.modelcontextprotocol/* field in every result’s _meta” (serverInfo)https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metano: not stamped on results; server/discover puts serverInfo top-level instead of _metan/aReplaces InitializeResult.serverInfo
meta-info-not-for-behavior-or-security2026-07-28bothSHOULD NOTmeta“Implementations SHOULD NOT use them to change the behavior of the client or server, and SHOULD NOT rely on them for security decisions.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayes (clientInfo not used for decisions, as far as read)yesEarlier revisions silent; legacy SDKs commonly branch on clientInfo.name
meta-subscription-id-on-listen-notifications2026-07-28serverMUSTmeta“the server MUST include io.modelcontextprotocol/subscriptionId in _meta” (listen-stream notifications)https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metano: id sent only in notifications/subscriptions/acknowledged params, not _meta of each pushed notificationn/aSubscriptions owner should confirm
meta-otel-keys-w3c-format2026-07-28bothMUSTmeta“When present, their values MUST follow W3C Trace Context and W3C Baggage formats respectively.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metan/a (does not emit); passes through? not verifiedn/aUnprefixed exception to the prefix rule
meta-extension-ids-follow-meta-rules2026-07-28bothMUSTversioning“Extension identifiers MUST follow the _meta key naming rules, with a mandatory prefix.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#extension-negotiationyes (io.modelcontextprotocol/tasks)n/a (declares no extensions)
meta-icons-client-mime-support2025-11-25..2026-07-28clientMUST / SHOULDmeta“Clients that support rendering icons MUST support at least the following MIME types:” png, jpeg; “SHOULD also support” svg, webphttps://modelcontextprotocol.io/specification/2026-07-28/basic/index#iconsn/an/a (renders no icons)
meta-icons-consumer-security2025-11-25..2026-07-28clientMUSTmeta“Consumers of icon metadata MUST take appropriate security precautions when handling icons to prevent compromise”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#iconsn/an/a (never fetches icons; passes metadata through)
meta-icons-reject-unsafe-schemes2025-11-25..2026-07-28clientMUSTmeta“Clients MUST reject icon URIs that use unsafe schemes and redirects, such as javascript:, file:, ftp:, ws:, or local app URI schemes.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#iconsn/an/a (does not dereference). Downstream host must do it
meta-icons-may-limit-size2025-11-25..2026-07-28clientMAYmeta“Consumers MAY set limits for image and content size.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#iconsn/an/a
meta-icons-same-origin2025-11-25..2026-07-28clientSHOULDmeta“Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the client/server or a trusted domain.” (schema)https://modelcontextprotocol.io/specification/2026-07-28/schema#iconn/an/aA proxy re-serving upstream icons changes “the server” origin — see Implied I12
json-schema-default-2020-122025-11-25..2026-07-28both(definitional)json-schema“When a schema does not include a $schema field, it defaults to JSON Schema 2020-12”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#schema-dialectyes (own schemas are dialect-neutral)n/aEarlier revisions: no dialect stated
json-schema-may-declare-dialect2025-11-25..2026-07-28bothMAYjson-schema“Schemas MAY include a $schema field to specify a different dialect”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#schema-dialectyes (passes upstream $schema through)yes
json-schema-support-2020-122025-11-25..2026-07-28bothMUSTjson-schema“Clients and servers MUST support JSON Schema 2020-12 for schemas without an explicit $schema field”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#implementation-requirementsnot verified: mcpx does not appear to validate tool arguments against schemas (no validator in mcpserver)n/a (passes through)
json-schema-validate-per-dialect-and-error-on-unsupported2025-11-25..2026-07-28bothMUSTjson-schema“Clients and servers MUST validate schemas according to their declared or default dialect. They MUST handle unsupported dialects gracefully by returning an appropriate error”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#implementation-requirementsno (no validation; a proxy forwarding opaque schemas arguably does not “handle” them)noUncertain what “validate schemas” binds a pass-through proxy to
json-schema-document-dialects2025-11-25..2026-07-28bothSHOULDjson-schema“Clients and servers SHOULD document which schema dialects they support”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#implementation-requirementsyes: documented in docs/schema-validation.md (Dialect policy table)yes: documented in docs/schema-validation.md (Dialect policy table)
json-schema-recommend-2020-122025-11-25..2026-07-28bothRECOMMENDEDjson-schema“Implementors are RECOMMENDED to use JSON Schema 2020-12.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#schema-dialectyesn/a
json-schema-schemas-valid2025-11-25..2026-07-28bothMUSTjson-schema“Schemas MUST be valid according to their declared or default dialect”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#schema-validationyes for own tools (compacted literal schemas, test-checked); upstream schemas passed through uncheckedn/a
json-schema-no-network-ref2026-07-28bothMUST NOTjson-schema“Implementations MUST NOT automatically dereference $ref values that resolve to a network URI.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#ref-resolutionyes (never dereferences)yesNew in 2026 (SEP-2106)
json-schema-remote-ref-opt-in-off-by-default2026-07-28bothMAY / MUST / SHOULDjson-schema“Implementations MAY offer an opt-in mode that fetches non-local $refs but it MUST be disabled by default and SHOULD enforce an allowlist of hosts”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#ref-resolutionn/an/a
json-schema-reject-unresolved-ref2026-07-28bothSHOULDjson-schema“Schemas that fail to validate due to an unresolved external $ref SHOULD be rejected rather than silently treated as permissive.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#ref-resolutionno (no validation)no
json-schema-bound-composition-cost2026-07-28bothSHOULDjson-schema“Implementations SHOULD apply reasonable bounds, such as a maximum schema depth, a cap on the total number of subschemas, or a per-validation time budget”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#composition-keyword-resource-useyes: schemaval bounds schema documents at 1 MiB (MaxSchemaBytes)yes: schemaval bounds schema documents at 1 MiB (MaxSchemaBytes)
versioning-server-unsupported-version-error2026-07-28serverMUSTversioning“it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#protocol-version-negotiationyes (-32022, data.supported, data.requested)n/a
versioning-unsupported-version-http-4002026-07-28serverMUSTerrors“For HTTP, the response status code MUST be 400 Bad Request.” (schema, UnsupportedProtocolVersionError)https://modelcontextprotocol.io/specification/2026-07-28/schema#unsupportedprotocolversionerrorno: ServeHTTP writes every JSON-RPC response with 200n/aSame MUST on HeaderMismatchError and MissingRequiredClientCapabilityError
errors-header-mismatch-http-4002026-07-28serverMUSTerrors“For HTTP, the response status code MUST be 400 Bad Request.” (schema, HeaderMismatchError)https://modelcontextprotocol.io/specification/2026-07-28/schema#headermismatcherrorpartly: 400, but body is {"error": "..."} not a JSON-RPC error with -32020n/aA dual-era client then cannot recognise it as a modern error and falls back to legacy — see I7
versioning-client-retry-with-supported2026-07-28clientSHOULDversioning“The client SHOULD select a mutually supported version from the supported list and retry the request, or surface an error to the user if no compatible version exists.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#protocol-version-negotiationn/ano: isVersionError stops and returns the error; no retry from data.supported
versioning-server-must-implement-discover2026-07-28serverMUSTversioning“Servers MUST implement server/discover.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#protocol-version-negotiationpartial: answers, but with protocolVersions (spec: supportedVersions), serverInfo top-level (spec: _meta), no ttlMs/cacheScopepartial: parses protocolVersions, so a spec-conformant server’s supportedVersions is missed → “no shared protocol version”Discover owner should re-check; mcpx client and server share the same wrong field name, so they interoperate with each other only
versioning-client-may-discover-first2026-07-28clientMAYversioning“Clients MAY call it before sending any other requests to learn the server’s supported versions up front, but are not required to”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#protocol-version-negotiationn/ayes (always does, in modern mode)
versioning-extension-fallback-or-reject2026-07-28bothMUSTversioning“the supporting party MUST either revert to core protocol behavior or reject the request with an appropriate error.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#extension-negotiationyes for tasks: a task handle only when the client asked with task; otherwise core resultn/aTasks extension says servers may return handles unsolicited (changelog item 6) — tasks owner to reconcile
versioning-extensions-document-fallback2026-07-28bothSHOULDversioning“Extensions SHOULD document their expected fallback behavior.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#extension-negotiationn/an/aBinds extension authors
versioning-dual-era-server-may2026-07-28serverMAYversioning“A server that wishes to support both legacy clients … and modern clients … MAY implement both behaviors.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#backward-compatibility-with-initialization-based-versionsyes (dual-era)n/a
versioning-era-cache2026-07-28clientSHOULD / MAYversioning“Clients SHOULD cache the result for the lifetime of the server process (stdio) or origin (HTTP), and MAY persist it across restarts of the same server configuration, re-probing if the cached assumption later fails.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#backward-compatibility-with-initialization-based-versionsn/apartly: Client.Era per connection (= per process on stdio); not cached per HTTP origin or across restarts (each pooled instance probes)See I1
versioning-modern-only-server-names-versions-to-initialize2026-07-28serverSHOULDversioning“A server that supports only modern versions SHOULD name the protocol versions it supports in any error it returns to an initialize request, on any transport”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#backward-compatibility-with-initialization-based-versionsn/a (dual-era); for an initialize naming an unsupported version it does name the legacy listn/a
versioning-modern-client-probe-legacy-stdio2026-07-28clientSHOULDversioning“On stdio, clients SHOULD send server/discover first to fail deterministically” (Modern client, Legacy server)https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#compatibility-matrixn/an/a unless protocol: force-discover; then discover is first (yes)Also stdio page: “probing is still RECOMMENDED”
versioning-dual-era-client-probe-discover-first-stdio2026-07-28clientSHOULDversioning“A client that supports both modern … and a legacy version … SHOULD probe with server/discover before sending any other request” (stdio page)https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#backward-compatibilityn/ano, by choice: probes legacy initialize first (“nearly every server in existence is legacy”)Versioning page defers to binding pages; the SHOULD lives in the transport page
versioning-fallback-not-keyed-to-one-code2026-07-28clientMUST NOTversioning“The fallback MUST NOT be keyed to one specific error code” (stdio page)https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#backward-compatibilityn/ayes (falls back on any error except -32022)mcpx inverts the probe order, so the rule applies to its legacy→modern fallback by analogy only
versioning-dual-era-http-detect2026-07-28clientMAY / SHOULDversioning“MAY detect which era the server implements by attempting a modern request first. On 400 Bad Request, the client SHOULD inspect the response body before falling back” (streamable-http page)https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#backward-compatibilityn/an/a (legacy first)Versioning matrix says “4xx”, binding page says “400” — Conflict C14
versioning-dual-era-server-selects-by-opening2026-07-28server(normative description)versioning“A request carrying modern per-request _meta is served statelessly according to this revision. An initialize request selects legacy semantics, scoped to the stdio process (stdio) or the session (HTTP)”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#compatibility-matrixyes, plus a third case the spec does not define: no _meta and no initialize → served as 2025-03-26n/aSee I4
versioning-dual-era-both-concurrently2026-07-28serverMAYversioning“A dual-era server MAY serve both eras concurrently on the same endpoint or process.”https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#compatibility-matrixyesn/a
lifecycle-stateless-no-reliance-on-prior-requests2026-07-28serverMUST NOTlifecycle“Servers MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity).”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessnessyes for version/caps of modern requestsn/aOpposite of legacy lifecycle, where the initialize context governs the whole session
lifecycle-stateless-multiple-conversations2026-07-28serverSHOULDlifecycle“Servers SHOULD be prepared to handle requests associated with multiple tasks, threads, or conversations.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessnessyesn/a
lifecycle-stateless-no-connection-reuse-required2026-07-28serverSHOULD NOTlifecycle“Servers SHOULD NOT require that a client reuse the same connection or process to perform related operations.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessnessno: requestState is HMAC-bound to the connection it was issued on, and a request with no session gets no token (questions go to the broker)n/aDeliberate security trade-off in mcpx (docs/protocol.md §3.3) — see I3
lifecycle-stateless-client-process-lifetime2026-07-28clientSHOULD NOTlifecycle“Clients SHOULD NOT use an individual task, thread, or conversation as the lifetime boundary for the stdio process.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessnessn/ayes (pooled by scope key, kept warm across calls)
lifecycle-stateless-explicit-handles2026-07-28bothMUSTlifecycle“State that needs to span multiple requests (e.g., long-running tasks, application-level handles) MUST be referenced by an explicit identifier the client passes on each request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessnessyes (task ids, requestState) — but HTTP also issues Mcp-Session-Id to modern clients on server/discover, which 2026 removedn/aChangelog item 1: servers “use explicit, server-minted handles passed as ordinary tool arguments”
messages-patterns-notifications-scoped-to-request2026-07-28serverMAYmessages“While the request is in flight, the server MAY send notifications scoped to it, such as notifications/progress and notifications/message.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/index#request-and-responseyesn/a
messages-resulttype-required2026-07-28serverMUSTmessages“The result MUST include a resultType field to indicate the type of the result.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responsesyes (stamped complete when request carried modern _meta)n/aIncludes EmptyResult (schema EmptyResult = Result)
messages-resulttype-extension-values2026-07-28bothMAY / MUSTmessages“Extensions MAY add additional ResultType values. The set of supported ResultType values MUST be created from the set defined in the core protocol and include any additional values of supported extensions that are advertised via capabilities.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#resulttypeyes (only core values)partly
messages-resulttype-unknown-invalid2026-07-28clientMUSTmessages“A resultType of any value unrecognized by the client MUST be considered invalid.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#resulttypen/ano: anything other than input_required is treated as complete (client.go:665)
messages-resulttype-absent-means-complete2026-07-28clientMUSTmessages“clients MUST treat an absent resultType as \"complete\".”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#resulttypen/ayesSchema words it as “a server implementing an earlier protocol version” — a 2026 server omitting it is non-conformant but indistinguishable; see I9
messages-resulttype-server-must-include2026-07-28serverMUSTmessages“Servers implementing this protocol version MUST include this field.” (schema, Result.resultType)https://modelcontextprotocol.io/specification/2026-07-28/schema#resultyes on modern requests; stripped for legacy by downgrade()n/a
authorization-http-should-conform-stdio-should-not2025-03-26..2026-07-28bothSHOULD / SHOULD NOTauthorization“Implementations using an HTTP-based transport SHOULD conform to this specification, whereas implementations using STDIO transport SHOULD NOT follow this specification”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#authnot-implemented-by-choice (no OAuth; loopback / unix socket)not-implemented-by-choice (no OAuth to upstreams)Listed here because it is on basic/index; authorization owner covers detail
authorization-custom-auth-may2024-11-05..2026-07-28bothMAYauthorization“clients and servers MAY negotiate their own custom authentication and authorization strategies.”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#authn/an/a2024-11-05: auth “not currently part of the core MCP specification”
security-implementors-guidelines2024-11-05..2026-07-28bothSHOULD(security; not in area list)“implementors SHOULD: 1. Build robust consent and authorization flows into their applications …”https://modelcontextprotocol.io/specification/2026-07-28/index#implementation-guidelinespartly (elicitation broker; no auth on /mcp beyond loopback)n/aThe “Key Principles” list uses lowercase must — informative. 2026 drops “LLM Sampling Controls” principle
deprecated-should-not-adopt2026-07-28bothSHOULD NOT / SHOULDversioning“new implementations SHOULD NOT adopt it, and existing implementations SHOULD migrate before the feature’s earliest removal.”https://modelcontextprotocol.io/specification/2026-07-28/deprecated#deprecatedRoots/Sampling/Logging/HTTP+SSE: mcpx relays sampling, accepts logging/setLevel, does not serve HTTP+SSEdeclares roots and (conditionally) sampling to upstreamsDeprecated: Roots, Sampling, Logging, DCR (2026-07-28); includeContext thisServer/allServers (2025-11-25); HTTP+SSE (2025-03-26)

Transports (stdio, HTTP+SSE, Streamable HTTP) and authorization

idrevssidelevelareaquoteurlmcpx-as-servermcpx-as-clientchanges
transport-jsonrpc-utf82025-03-26..2026-07-28bothMUSTmessages“JSON-RPC messages MUST be UTF-8 encoded.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#messagesyes — Go encoding/json emits UTF-8yes — sameAbsent from 2024-11-05 transports page (JSON-RPC implies it). Moved to transports/index in 2026.
transport-client-should-support-stdio2024-11-05..2025-11-25clientSHOULDtransport-stdio“Clients SHOULD support stdio whenever possible.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transportsn/ayes — StdioTransportSentence dropped in 2026-07-28 (index lists stdio without the SHOULD).
transport-custom-preserve-jsonrpc-and-lifecycle2024-11-05..2025-11-25bothMUSTtransport-stdio“MUST ensure they preserve the JSON-RPC message format and lifecycle requirements defined by MCP”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#custom-transportsyes — mcpx’s only non-standard surface is /v1 REST, which is not an MCP transportn/aReplaced in 2026 by next row (“lifecycle” → “message patterns” + “per-request metadata model”).
transport-custom-preserve-patterns-and-metadata2026-07-28bothMUSTtransport-stdio“MUST preserve the JSON-RPC message format, the message patterns, and the per-request metadata model”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#custom-transportsn/a (no custom MCP transport)n/a
transport-custom-document-patterns2024-11-05..2026-07-28bothSHOULDtransport-stdio“Custom transports SHOULD document their specific connection establishment and message exchange patterns”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#custom-transportsn/an/a2026 wording: “connection establishment, message framing, and cancellation patterns”.
transport-custom-byte-stream-reuses-stdio-framing2026-07-28bothSHOULDtransport-stdio“Custom transports that run over a reliable bidirectional byte stream … SHOULD reuse the stdio framing”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#custom-transportsn/an/aAlso stated on the stdio page.
transport-binding-message-directions2026-07-28bothMUSTmessages“A binding MUST deliver client-sent requests and notifications to the server, and server-sent responses and notifications to the client.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#messagesyes for modern clients; for legacy sessions mcpx also sends requests (allowed by the legacy revision)yesNew: no server→client requests, no client→server responses. Contradicts 2025-* (see Conflicts C1).
transport-binding-may-mirror-metadata2026-07-28bothMAYmessages“A binding MAY additionally mirror selected body fields into envelope metadata.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#request-metadata——“The body remains the source of truth”.
stdio-no-embedded-newlines2024-11-05..2026-07-28bothMUST NOTtransport-stdio“Messages are delimited by newlines, and MUST NOT contain embedded newlines.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdioyes — json.Encoder escapes newlinesyes
stdio-message-may-be-batch2025-03-26bothMAY (implied)transport-stdio“Messages may be JSON-RPC requests, notifications, responses—or a JSON-RPC batch containing one or more requests and/or notifications.”https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdioyes — ServeStdio answers a 2025-03-26 batch with an array (isBatch, internal/mcpserver/server.go); tested, see conformance-matrix.mdpartial — never sends batches; does not parse batch replies on stdioRemoved 2025-06-18 (Conflicts C2).
stdio-message-is-single2025-06-18..2026-07-28bothMUST (implied)transport-stdio“Messages are individual JSON-RPC requests, notifications, or responses.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdioyesyes2026: “Each message is a single JSON-RPC request, notification, or response.”
stdio-server-may-log-to-stderr2024-11-05..2026-07-28serverMAYtransport-stdio“The server MAY write UTF-8 strings to its standard error (stderr) for logging purposes.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdioyesn/a2025-11-25+ broadened: “for any logging purposes including informational, debug, and error messages”.
stdio-client-may-capture-stderr2024-11-05..2026-07-28clientMAYtransport-stdio“Clients MAY capture, forward, or ignore this logging.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdion/ayes — ring buffer + optional StderrTo
stdio-client-stderr-not-error2025-11-25..2026-07-28clientSHOULD NOTtransport-stdio“SHOULD NOT assume stderr output indicates error conditions.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdion/ayes — stderr only attached to error messages when the process already failedNew in 2025-11-25.
stdio-server-stdout-only-mcp2024-11-05..2026-07-28serverMUST NOTtransport-stdio“The server MUST NOT write anything to its stdout that is not a valid MCP message.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdioyes (logs go to stderr; not exhaustively verified)n/a
stdio-client-stdin-only-mcp2024-11-05..2026-07-28clientMUST NOTtransport-stdio“The client MUST NOT write anything to the server’s stdin that is not a valid MCP message.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdion/ayes
stdio-client-no-responses2026-07-28clientMUST NOTtransport-stdio“The client MUST NOT write JSON-RPC responses.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#sending-messagesn/ayes to a modern server (mcpx answers only legacy servers’ requests)Legal and required in 2024-11-05..2025-11-25 (C1).
stdio-server-no-requests2026-07-28serverMUST NOTtransport-stdio“The server MUST NOT write JSON-RPC requests to stdout.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#receiving-messagesyes — modern clients get input_required; wire requests only to legacy clientsn/aC1.
stdio-client-correlates-subscription-id2026-07-28clientMUSTtransport-stdio“Clients MUST correlate these using the io.modelcontextprotocol/subscriptionId field in _meta”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#receiving-messagesn/auncertain — not checked whether mcpx opens subscriptions/listen upstream
stdio-client-cancel-by-notification2026-07-28clientMUSTtransport-stdio“To cancel an in-flight request, the client MUST send a notifications/cancelled notification referencing the request’s ID.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#cancellationn/ayes — sent on its own background contextAlso cancellation page.
stdio-server-stop-cancelled-work2026-07-28serverSHOULDtransport-stdio“Servers SHOULD stop work on a cancelled request as soon as practical”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#cancellationyes — notifications/cancelled cancels the request context the backend runs under (Conn.track); tested, see conformance-matrix.mdn/a
stdio-server-silent-after-cancel2026-07-28serverMUST NOTtransport-stdio“MUST NOT send any further messages for it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#cancellationuncertain — mcpx may still write the response when the upstream call returnsn/aEarlier revs: cancellation page says receivers “SHOULD” not respond (not my area).
stdio-server-cancelled-only-for-listen2026-07-28serverMUST NOTtransport-stdio“On stdio, the server also sends this notification, solely to terminate a subscriptions/listen stream … Servers MUST NOT use this notification to cancel any other request.” (schema.ts CancelledNotification)https://modelcontextprotocol.io/specification/2026-07-28/schema#cancellednotificationuncertain — check stopListen on stdion/aConflicts with cancellation page scope (C12).
stdio-client-shutdown-sequence2026-07-28clientSHOULDtransport-stdio“The client SHOULD initiate shutdown by: 1. Closing the input stream … 2. Waiting … 3. … forcibly terminating”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#shutdownn/ayes — close stdin, SIGTERM pgid, then SIGKILLEarlier revs put this on the lifecycle page (not my area).
stdio-server-exit-on-eof2026-07-28serverSHOULDtransport-stdio“Servers SHOULD exit promptly when their standard input is closed or reads return end-of-file.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#shutdownyes — scan loop ends, waits for in-flightn/a
stdio-server-may-close-stdout2026-07-28serverMAYtransport-stdio“The server MAY initiate shutdown by closing its output stream to the client and exiting.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#shutdown—must tolerate — yes (Recv returns exit error)
stdio-client-restart-crashed-server2026-07-28clientSHOULDtransport-stdio“If the server process exits unexpectedly, the client SHOULD restart it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#unexpected-terminationn/auncertain — pool respawns on next use (not verified)Relies on statelessness; for a legacy server the restart needs a new initialize (implied).
stdio-dual-era-client-probe-discover2026-07-28clientSHOULDtransport-stdio“SHOULD probe with server/discover before sending any other request, setting its preferred modern version in _meta.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#backward-compatibilityn/anot-implemented-by-choice — default probes legacy initialize first (protocol: prefer-discover flips it)Versioning page matrix repeats this.
stdio-no-fallback-on-modern-error2026-07-28clientMUST NOT (informal “Do not”)transport-stdio“Use one of the versions in its advertised supported list. Do not fall back to initialize.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#backward-compatibilityn/ayes — isVersionError stops the fallback (string match on -32022)
stdio-modern-only-client-probe2026-07-28clientRECOMMENDEDtransport-stdio“A client that only supports modern versions does not need to probe, but probing is still RECOMMENDED”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#backward-compatibilityn/aforce-discover probes discover — yes
http-sse-server-validate-origin2024-11-05serverMUSTtransport-http-sse“Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#security-warningnot-implemented-by-choice — mcpx does not serve HTTP+SSEn/a
http-sse-server-bind-localhost2024-11-05serverSHOULDtransport-http-sse“When running locally, servers SHOULD bind only to localhost (127.0.0.1)”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#security-warningnot-implemented-by-choicen/a
http-sse-server-authenticate2024-11-05serverSHOULDtransport-http-sse“Servers SHOULD implement proper authentication for all connections”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#security-warningnot-implemented-by-choicen/a
http-sse-server-two-endpoints2024-11-05serverMUSTtransport-http-sse“The server MUST provide two endpoints: 1. An SSE endpoint … 2. A regular HTTP POST endpoint”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-ssenot-implemented-by-choice — floor is 2025-03-26n/a
http-sse-server-endpoint-event2024-11-05serverMUSTtransport-http-sse“When a client connects, the server MUST send an endpoint event containing a URI for the client to use for sending messages.”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-ssenot-implemented-by-choicegap if a 2024-11-05 remote is configured — no SSE client
http-sse-client-post-to-endpoint2024-11-05clientMUSTtransport-http-sse“All subsequent client messages MUST be sent as HTTP POST requests to this endpoint.”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-ssen/anot-implemented (no HTTP+SSE client)
http-sse-server-messages-as-sse-events2024-11-05serverMUST (implied)transport-http-sse“Server messages are sent as SSE message events, with the message content encoded as JSON in the event data.”https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-ssenot-implemented-by-choicenot-implemented
http-sse-deprecated-no-new-adoption2026-07-28bothSHOULD NOTtransport-http-sse“New implementations SHOULD NOT adopt it; existing implementations SHOULD migrate to Streamable HTTP.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#httpsse-transport-2024-11-05yesyes“Replaced” since 2025-03-26 without a keyword; formally Deprecated (SEP-2596) in 2026.
http-sse-server-backcompat-host-both2025-03-26..2026-07-28servershould (lowercase)transport-http-sse“Continue to host both the SSE and POST endpoints of the old transport, alongside the new "MCP endpoint"”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#httpsse-transport-2024-11-05not-implemented-by-choicen/aOptional; “possible to combine the old POST endpoint and the new MCP endpoint”.
http-sse-client-backcompat-probe-any-4xx2025-03-26..2025-06-18clientshould (lowercase)transport-http-sse“If it fails with an HTTP 4xx status code (e.g., 405 Method Not Allowed or 404 Not Found): Issue a GET request … expecting … an endpoint event”https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#backwards-compatibilityn/anot-implemented — no GET fallbackNarrowed twice (C9).
http-sse-client-backcompat-probe-400-404-4052025-11-25clientshould (lowercase)transport-http-sse“If it fails with the following HTTP status codes "400 Bad Request", "404 Not Found" or "405 Method Not Allowed"”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#backwards-compatibilityn/anot-implementedC9.
http-sse-client-backcompat-probe-non-modern-body2026-07-28clientshould (lowercase)transport-http-sse“fails with HTTP status code 400 Bad Request, 404 Not Found, or 405 Method Not Allowed and the response body is not a recognized modern JSON-RPC error”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#httpsse-transport-2024-11-05n/anot-implementedC9. Probe is now “POST a request” (not InitializeRequest).
streamable-http-server-single-endpoint-post-get2025-03-26..2025-11-25serverMUSTtransport-streamable-http“The server MUST provide a single HTTP endpoint path … that supports both POST and GET methods.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-httpyes — GET “supported” by answering 405, which the GET section permitsn/a
streamable-http-server-single-endpoint-post2026-07-28serverMUSTtransport-streamable-http“The server MUST provide a single HTTP endpoint path … that supports POST.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-httpyesn/aGET dropped.
streamable-http-server-validate-origin2025-03-26..2026-07-28serverMUSTtransport-streamable-http“Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#security–endpointyes — ServeHTTP refuses a foreign Origin (originAllowed, internal/mcpserver/server.go, #198); /v1 too (internal/daemon/origin.go, #267); tested, see conformance-matrix.mdn/a
streamable-http-server-403-invalid-origin2025-11-25..2026-07-28serverMUSTtransport-streamable-http“If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#security–endpointyes — 403 with an id-less JSON-RPC error (as above); tested, see conformance-matrix.mdn/aAdded 2025-11-25 (PR #1439). Body MAY be JSON-RPC error with no id.
streamable-http-server-bind-localhost2025-03-26..2026-07-28serverSHOULDtransport-streamable-http“When running locally, servers SHOULD bind only to localhost (127.0.0.1)”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#security–endpointyes — daemon default loopback; --address 0.0.0.0 is opt-in with a warningn/a
streamable-http-server-authenticate2025-03-26..2026-07-28serverSHOULDtransport-streamable-http“Servers SHOULD implement proper authentication for all connections.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#security–endpointnot-implemented-by-choice — /mcp is unauthenticated, like /v1n/a
streamable-http-client-post-every-message2025-03-26..2026-07-28clientMUSTtransport-streamable-http“Every JSON-RPC message sent from the client MUST be a new HTTP POST request to the MCP endpoint.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesn/ayes
streamable-http-client-accept-both2025-03-26..2026-07-28clientMUSTtransport-streamable-http“The client MUST include an Accept header, listing both application/json and text/event-stream”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesdoes not check (lenient)yes
streamable-http-post-body-may-batch2025-03-26clientMUSTtransport-streamable-http“The body of the POST request MUST be one of the following: A single JSON-RPC request, notification, or response; An array batching one or more requests and/or notifications; An array batching one or more responses”https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#sending-messages-to-the-servergap for 2025-03-26 — batch body → parse error 400yes (never batches); parses batch repliesBatching removed 2025-06-18 (C2).
streamable-http-post-body-single2025-06-18..2025-11-25clientMUSTtransport-streamable-http“The body of the POST request MUST be a single JSON-RPC request, notification, or response.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-serveryesyes
streamable-http-post-body-request-or-notification2026-07-28clientMUST / MUST NOTtransport-streamable-http“The body of the HTTP POST MUST be a single JSON-RPC request or notification. The client MUST NOT send JSON-RPC responses.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesaccepts responses anyway (for legacy sessions) — liberal acceptyes to modern serversC1.
streamable-http-client-sends-metadata-headers2026-07-28clientMUSTtransport-streamable-http“The client MUST include the request metadata headers on each POST request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesn/ayes — Mcp-Method/Mcp-Name and Mcp-Param-* on every modern POST (internal/mcpclient/headers.go); tested, see conformance-matrix.mdNotification POST headers undefined (C11).
streamable-http-server-202-for-non-request2025-03-26..2026-07-28serverMUSTtransport-streamable-http“If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesyes — notifications and client responses → 202tolerates 202 and 204Input set: responses/notifications (2025-03-26: “solely of (any number of)”), response/notification (2025-06-18..2025-11-25), notification only (2026).
streamable-http-server-error-status-if-rejected2025-03-26..2026-07-28serverMUSTtransport-streamable-http“If the server cannot accept the input, it MUST return an HTTP error status code (e.g., 400 Bad Request).”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesyes — unknown reply id → 400 (body is not JSON-RPC; MAY)n/aBody MAY be a JSON-RPC error with no id.
streamable-http-server-json-or-sse-for-request2025-03-26..2026-07-28serverMUSTtransport-streamable-http“the server MUST either return Content-Type: text/event-stream … or Content-Type: application/json”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesyes — JSON unless it must ask/stream, then SSE lazilyn/a
streamable-http-client-supports-json-and-sse2025-03-26..2026-07-28clientMUSTtransport-streamable-http“The client MUST support both these cases.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messagesn/ayes
streamable-http-sse-includes-response2025-03-26..2025-11-25serverSHOULDtransport-streamable-http“The SSE stream SHOULD eventually include a JSON-RPC response for the JSON-RPC request sent in the POST body.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-serveryesn/a2025-03-26: one response per request, MAY be batched.
streamable-http-post-sse-may-carry-requests2025-03-26..2025-11-25serverMAYtransport-streamable-http“The server MAY send JSON-RPC requests and notifications before sending the JSON-RPC response. These messages SHOULD relate to the originating client request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-serveryes — elicitation/create/sampling on the POST’s stream (legacy sessions)yes — handles requests found on the streamForbidden in 2026 (C1).
streamable-http-sse-notifications-must-relate2026-07-28serverMUSTtransport-streamable-http“These notifications MUST relate to the originating client request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyesn/aWas SHOULD in 2025-* (C3).
streamable-http-sse-no-requests2026-07-28serverMUST NOTtransport-streamable-http“The server MUST NOT send independent JSON-RPC requests on this stream.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyes for modern requestsn/aC1.
streamable-http-sse-not-closed-before-response2025-03-26..2025-06-18serverSHOULD NOTtransport-streamable-http“The server SHOULD NOT close the SSE stream before sending the JSON-RPC response for the received JSON-RPC request, unless the session expires.”https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-serveryesn/aReplaced 2025-11-25 by polling rules (C4).
streamable-http-server-may-terminate-on-session-expiry2025-11-25serverMAYtransport-streamable-http“The server MAY terminate the SSE stream if the session expires.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server——
streamable-http-server-terminates-sse-after-response2025-03-26..2026-07-28serverSHOULDtransport-streamable-http“After the JSON-RPC response has been sent, the server SHOULD terminate the SSE stream.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyesn/a2025-03-26/06-18 “close”; 2025-11-25 “terminate” (stream vs connection distinction); 2026 “The final JSON-RPC response SHOULD terminate the stream.” Not for subscriptions/listen streams.
streamable-http-sse-priming-event2025-11-25serverSHOULDtransport-streamable-http“The server SHOULD immediately send an SSE event consisting of an event ID and an empty data field in order to prime the client to reconnect”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servernot-implemented-by-choice — no event ids, no resumabilitymust tolerate an empty data event — yes (skipped)Removed 2026 (C5).
streamable-http-server-may-close-connection-keep-stream2025-11-25serverMAYtransport-streamable-http“the server MAY close the connection (without terminating the SSE stream) at any time”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servernot usedgap — a server that does this loses mcpx the response (no reconnect)C4, C5.
streamable-http-client-polls-after-close2025-11-25clientSHOULDtransport-streamable-http“The client SHOULD then "poll" the SSE stream by attempting to reconnect.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servern/ayes — reconnects with GET and Last-Event-ID after retry (internal/mcpclient/http.go); tested, see conformance-matrix.md
streamable-http-server-sends-retry-before-close2025-11-25serverSHOULDtransport-streamable-http“it SHOULD send an SSE event with a standard retry field before closing the connection.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servern/a (never closes early)n/a
streamable-http-client-respects-retry2025-11-25clientMUSTtransport-streamable-http“The client MUST respect the retry field, waiting the given number of milliseconds before attempting to reconnect.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servern/avacuously yes (never reconnects); retry: lines ignored
streamable-http-disconnect-not-cancel2025-03-26..2025-11-25bothSHOULD NOTtransport-streamable-http“Disconnection SHOULD NOT be interpreted as the client cancelling its request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-serveruncertain — handler runs on r.Context(), so a disconnect cancels mcpx-side waiting for a legacy client toon/aInverted in 2026 (C6).
streamable-http-client-cancel-by-notification2025-03-26..2025-11-25clientSHOULDtransport-streamable-http“To cancel, the client SHOULD explicitly send an MCP CancelledNotification.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servern/ayes2026: “no notifications/cancelled message is expected” (C6).
streamable-http-server-may-resume2025-03-26..2025-11-25serverMAYtransport-streamable-http“To avoid message loss due to disconnection, the server MAY make the stream resumable.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-servernot-implemented-by-choicen/aRemoved 2026 (C5).
streamable-http-close-is-cancel2026-07-28serverMUSTtransport-streamable-http“Closing the SSE response stream MUST be treated by the server as cancellation of that request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellationpartial — request ctx cancels; upstream call not interruptedn/aCancellation page: “The server MUST treat a client disconnect as cancellation” (covers JSON responses too). C6.
streamable-http-stop-cancelled-work2026-07-28serverSHOULDtransport-streamable-http“The server SHOULD stop work on the cancelled request as soon as practical”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellationyes — a closed POST stream cancels the backend call; tested, see conformance-matrix.mdn/a
streamable-http-silent-after-cancel2026-07-28serverMUST NOTtransport-streamable-http“MUST NOT send any further messages for it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellationyes (stream is gone)n/a
streamable-http-client-may-get2025-03-26..2025-11-25clientMAYtransport-streamable-http“The client MAY issue an HTTP GET to the MCP endpoint.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-servern/anot-implemented-by-choice — never opens GET (so unsolicited list_changed from legacy HTTP upstreams is missed)Removed 2026.
streamable-http-get-accept-sse2025-03-26..2025-11-25clientMUSTtransport-streamable-http“The client MUST include an Accept header, listing text/event-stream”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-servern/an/a (no GET)
streamable-http-get-sse-or-4052025-03-26..2025-11-25serverMUSTtransport-streamable-http“The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, or else return HTTP 405 Method Not Allowed”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-serveryes — 405n/a
streamable-http-get-stream-unrelated2025-03-26..2025-11-25serverSHOULDtransport-streamable-http“These messages SHOULD be unrelated to any concurrently-running JSON-RPC request from the client.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-servern/a (405)n/a
streamable-http-get-no-responses2025-03-26..2025-11-25serverMUST NOTtransport-streamable-http“The server MUST NOT send a JSON-RPC response on the stream unless resuming a stream associated with a previous client request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-servern/an/a
streamable-http-get-close-any-time2025-03-26..2025-11-25bothMAYtransport-streamable-http“The server MAY close the SSE stream at any time. The client MAY close the SSE stream at any time.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server——2025-11-25 adds: closing connection without terminating stream SHOULD follow polling (send retry).
streamable-http-multiple-streams2025-03-26..2025-11-25clientMAYtransport-streamable-http“The client MAY remain connected to multiple SSE streams simultaneously.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#multiple-connectionsmust handle — yes (one goroutine per stream)yes
streamable-http-no-broadcast2025-03-26..2025-11-25serverMUST / MUST NOTtransport-streamable-http“The server MUST send each of its JSON-RPC messages on only one of the connected streams; that is, it MUST NOT broadcast”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#multiple-connectionsyes — each frame goes to its request’s streamn/a2026: implied by request-scoped streams, but listen stream + request stream could both qualify (see Implied).
streamable-http-event-id-unique2025-03-26..2025-11-25serverMUSTtransport-streamable-http“If present, the ID MUST be globally unique across all streams within that session—or all streams with that specific client”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redeliveryn/a (no ids)n/a
streamable-http-event-id-encodes-stream2025-11-25serverSHOULDtransport-streamable-http“Event IDs SHOULD encode sufficient information to identify the originating stream”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redeliveryn/an/a
streamable-http-client-resume-with-last-event-id2025-03-26..2025-11-25clientSHOULDtransport-streamable-http“it SHOULD issue an HTTP GET to the MCP endpoint, and include the Last-Event-ID header”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redeliveryn/anot-implemented2025-11-25: “Resumption is always via HTTP GET with Last-Event-ID” regardless of POST/GET origin.
streamable-http-server-no-cross-stream-replay2025-03-26..2025-11-25serverMUST NOTtransport-streamable-http“The server MUST NOT replay messages that would have been delivered on a different stream.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redeliveryn/an/a
streamable-http-no-resumability2026-07-28both— (statement)transport-streamable-http“Resumable SSE streams via Last-Event-ID are not supported.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyesyesC5.
streamable-http-client-reissue-broken-request2026-07-28clientMUST (changelog)transport-streamable-http“A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID”https://modelcontextprotocol.io/specification/2026-07-28/changelogn/anot verified — pool retry behaviour unknownMUST exists only in the changelog, not on the transport page (C10).
streamable-http-server-may-assign-session2025-03-26..2025-11-25serverMAYtransport-streamable-http“A server using the Streamable HTTP transport MAY assign a session ID at initialization time, by including it in an Mcp-Session-Id header on the HTTP response containing the InitializeResult.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementyes — on initializen/a2025-11-25 spells it MCP-Session-Id (case-insensitive). mcpx also mints on server/discover (2026: SHOULD NOT).
streamable-http-session-id-secure2025-03-26..2025-11-25serverSHOULDtransport-streamable-http“The session ID SHOULD be globally unique and cryptographically secure”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementyes — 16 random bytes (falls back to literal sess-0 if crypto/rand fails)n/a
streamable-http-session-id-visible-ascii2025-03-26..2025-11-25serverMUSTtransport-streamable-http“The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E).”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementyes — sess-<hex>n/a
streamable-http-client-handles-session-securely2025-11-25clientMUSTtransport-streamable-http“The client MUST handle the session ID in a secure manner”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementn/ayes (in memory only; trace redaction not checked)New 2025-11-25.
streamable-http-client-echoes-session2025-03-26..2025-11-25clientMUSTtransport-streamable-http“clients using the Streamable HTTP transport MUST include it in the Mcp-Session-Id header on all of their subsequent HTTP requests.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementn/ayes — also sends it to modern servers (2026 server SHOULD ignore)
streamable-http-server-400-missing-session2025-03-26..2025-11-25serverSHOULDtransport-streamable-http“Servers that require a session ID SHOULD respond to requests without an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementn/a — mcpx does not require one (stateless fallback)n/a
streamable-http-server-404-terminated-session2025-03-26..2025-11-25serverMUSTtransport-streamable-http“The server MAY terminate the session at any time, after which it MUST respond to requests containing that session ID with HTTP 404 Not Found.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementyes — an ended or unknown session id is 404; tested, see conformance-matrix.mdn/a
streamable-http-client-reinit-on-4042025-03-26..2025-11-25clientMUSTtransport-streamable-http“it MUST start a new session by sending a new InitializeRequest without a session ID attached.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementn/ayes — a session 404 triggers one re-initialize (no session id on it) and one retry on the new session; tested, see conformance-matrix.mdUnder 2026 a 404 means “method not found” (C7).
streamable-http-client-delete-session2025-03-26..2025-11-25clientSHOULDtransport-streamable-http“Clients that no longer need a particular session … SHOULD send an HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementaccepts — 204yes — on Close
streamable-http-server-may-405-delete2025-03-26..2025-11-25serverMAYtransport-streamable-http“The server MAY respond to this request with HTTP 405 Method Not Allowed”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-managementchooses 204tolerates any status
streamable-http-protocol-version-header-is-negotiated2025-06-18..2025-11-25clientSHOULDtransport-streamable-http“The protocol version sent by the client SHOULD be the one negotiated during initialization.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-headern/ayes — the header carries the negotiated revision; tested, see conformance-matrix.md
streamable-http-server-assumes-2025-03-262025-06-18..2025-11-25serverSHOULDtransport-streamable-http“if the server does not receive an MCP-Protocol-Version header, and has no other way to identify the version … the server SHOULD assume protocol version 2025-03-26.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-headeryes — 2025-03-26 is mcpx’s floor/defaultn/a2026 makes it MAY and conditional (C8).
streamable-http-server-400-unsupported-version-header2025-06-18..2025-11-25serverMUSTtransport-streamable-http“If the server receives a request with an invalid or unsupported MCP-Protocol-Version, it MUST respond with 400 Bad Request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-headeryes — an unsupported MCP-Protocol-Version is 400; tested, see conformance-matrix.mdn/a
streamable-http-auth-discovery-version-header2025-03-26clientSHOULDauthorization“MCP clients SHOULD include the header MCP-Protocol-Version: <protocol-version> during Server Metadata Discovery”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery-headersn/an/a (no OAuth)Header appears in auth before the transport defines it (2025-06-18); example value is 2024-11-05 (C13).
streamable-http-protocol-version-header-every-post2026-07-28clientMUSTtransport-streamable-http“Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-headern/ayesNow includes the first request (no handshake).
streamable-http-protocol-version-header-matches-meta2026-07-28bothMUSTtransport-streamable-http“The header value MUST match the io.modelcontextprotocol/protocolVersion field … the server MUST reject the request with 400 Bad Request and a HeaderMismatch JSON-RPC error”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-headerpartial — 400, but body {"error": "..."}, not JSON-RPC -32020yes — header follows frame _metaschema.ts RequestMetaObject repeats (without naming HeaderMismatch).
streamable-http-unsupported-version-4002026-07-28serverMUSTtransport-streamable-http“it MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError listing its supported versions.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-headeruncertain — -32022 exists; HTTP status for it not verifiedn/aschema.ts: “For HTTP, the response status code MUST be 400 Bad Request.”
streamable-http-missing-capability-4002026-07-28serverMUSTtransport-streamable-http“For HTTP, the response status code MUST be 400 Bad Request.” (schema.ts MissingRequiredClientCapabilityError)https://modelcontextprotocol.io/specification/2026-07-28/schema#missingrequiredclientcapabilityerroruncertain — mcpx asks via broker rather than erroringn/a
streamable-http-unknown-method-4042026-07-28serverMUSTtransport-streamable-http“If the server does not implement the requested RPC method, it MUST respond with 404 Not Found and a JSON-RPC error with code -32601”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-headeryes — -32601 is HTTP 404 on the modern path; tested, see conformance-matrix.mdmust not mistake it for a dead session/legacy server — partial (any 404 → error)C7.
streamable-http-missing-version-header-legacy2026-07-28serverMAY / MUSTtransport-streamable-http“MAY treat a request that omits the header as protocol version 2025-03-26. A server that does not support such clients MUST reject a request without the header”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-headeryes (MAY branch)n/aC8.
streamable-http-standard-headers-required2026-07-28clientREQUIREDtransport-streamable-http“Mcp-Method … All requests; Mcp-Name … tools/call, resources/read, prompts/get requests. These headers are REQUIRED for compliance.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#standard-request-headersn/ayes (standardHeaders, internal/mcpclient/headers.go); tested, see conformance-matrix.mdSEP-2243.
streamable-http-mcp-name-base642026-07-28clientMUSTtransport-streamable-http“If the Mcp-Name source value cannot be safely represented as a plain ASCII header value, clients MUST encode it using the Base64 sentinel format”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#standard-request-headersn/ayes (mcpheaders.Encode, internal/mcpheaders); tested, see conformance-matrix.md
streamable-http-x-mcp-header-server-may2026-07-28serverMAYtransport-streamable-http“MCP servers MAY designate specific tool parameters to be mirrored into HTTP headers using an x-mcp-header extension property”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#custom-headers-from-tool-parametersnot used; but mcpx re-exports upstream tool schemas — an upstream x-mcp-header passes through, obliging mcpx’s HTTP clients and mcpx’s own validation (see Implied)—
streamable-http-x-mcp-header-client-must-mirror2026-07-28clientMUSTtransport-streamable-http“clients MUST support this feature. When a server’s tool definition includes x-mcp-header annotations, conforming clients MUST mirror the designated parameter values into HTTP headers.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#custom-headers-from-tool-parametersn/ayes (mcpheaders.Values, internal/mcpheaders); tested, see conformance-matrix.md
streamable-http-x-mcp-header-constraints2026-07-28serverMUST / MUST NOTtransport-streamable-http“MUST NOT be empty; MUST match HTTP field-name token syntax; … case-insensitively unique; … only … primitive types … Parameters with type number are not permitted; … statically reachable”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extensionn/a unless passing through upstream annotationsn/aInteger values MUST be within ±(2^53−1).
streamable-http-client-rejects-invalid-x-mcp-header2026-07-28clientMUSTtransport-streamable-http“Clients using the Streamable HTTP transport MUST reject tool definitions where any x-mcp-header value violates these constraints. … MUST exclude the invalid tool from the result of tools/list.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extensionn/ayes (checkToolHeaders, internal/mcpclient/modern.go); tested, see conformance-matrix.md“SHOULD log a warning”; stdio clients MAY ignore annotations.
streamable-http-param-value-encoding2026-07-28clientMUSTtransport-streamable-http“Clients MUST encode parameter values before including them in HTTP headers … clients MUST use Base64 encoding of the UTF-8 representation”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#value-encodingn/ayes (mcpheaders.Encode); tested, see conformance-matrix.mdMarkers =?base64?…?= “MUST appear exactly as shown (lowercase)”; sentinel-lookalike plain values MUST also be encoded.
streamable-http-server-decodes-before-compare2026-07-28serverMUSTtransport-streamable-http“servers MUST decode an encoded Mcp-Name or Mcp-Param-{Name} value before comparing it to the corresponding request body value”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#value-encodingyes — encoded header values are decoded before comparison; tested, see conformance-matrix.mdn/aAlso intermediaries “that need to inspect”.
streamable-http-client-header-construction2026-07-28clientMUSTtransport-streamable-http“When constructing a tools/call request via HTTP transport, the client MUST: 1. Extract … 2. Append the Mcp-Method header … 5. Append a Mcp-Param-{Name}: {Value} header”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#client-behaviorn/ayes (mcpheaders.Values); tested, see conformance-matrix.mdOmit header when value null/absent (table).
streamable-http-client-relist-on-header-mismatch2026-07-28clientSHOULDtransport-streamable-http“the client SHOULD call tools/list to check for changes to the tool’s inputSchema, then retry”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#client-behaviorn/ayes — re-lists tools and retries once on -32020 (toolCallHeaders, internal/mcpclient/modern.go); tested, see conformance-matrix.md
streamable-http-intermediary-forwards-unknown-param2026-07-28intermediaryMUSTtransport-streamable-http“Intermediate servers that do not recognize an Mcp-Param-{Name} header MUST forward it and otherwise ignore it”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-behavior-for-custom-headersn/a — mcpx is not an HTTP intermediary; it terminates MCP and re-originatesn/a
streamable-http-server-validates-param-headers2026-07-28serverMUSTtransport-streamable-http“Any server that processes the message body MUST validate that encoded header values … match … Servers MUST reject requests with a 400 Bad Request HTTP status and JSON-RPC error code -32020”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-behavior-for-custom-headersyes — Mcp-Param-* headers are checked against the x-mcp-header annotations of the tool named (checkParamHeaders, internal/mcpserver/transport.go, via mcpheaders.Check); tested, see conformance-matrix.mdn/aServer MUST reject a recognized Mcp-Param with invalid chars; MUST reject when header omitted but body has value; MUST NOT expect header for null/absent.
streamable-http-header-names-case-insensitive2026-07-28bothMUSTtransport-streamable-http“Clients and servers MUST use case-insensitive comparisons for header names. Header values … are case-sensitive.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#case-sensitivityyes (Go canonicalizes)yesExplains why Mcp-Session-Id→MCP-Session-Id rename (2025-11-25) is harmless.
streamable-http-server-rejects-header-body-mismatch2026-07-28serverMUSTtransport-streamable-http“Servers that process the request body MUST reject requests where the values specified in the headers do not match the corresponding values in the request body.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validationpartial — only versionn/aIncludes “A required standard header … is missing”.
streamable-http-header-mismatch-400-jsonrpc2026-07-28serverMUSTtransport-streamable-http“servers MUST return HTTP status 400 Bad Request and MUST include a JSON-RPC error response using the following error code: -32020”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validationyes — 400 with a JSON-RPC -32020 body carrying the request id; tested, see conformance-matrix.mdn/aCode was -32001 in drafts (changelog minor 12).
streamable-http-numeric-compare2026-07-28serverSHOULDtransport-streamable-http“servers SHOULD compare the header value and the body value numerically rather than as strings”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validationn/an/a
streamable-http-intermediary-http-error2026-07-28intermediaryMUSTtransport-streamable-http“Intermediaries MUST return an appropriate HTTP error status … but are not required to return a JSON-RPC error response.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validationn/aclient must tolerate non-JSON-RPC 400 — yes
streamable-http-intermediary-reject-unvalidated-version2026-07-28intermediarySHOULDtransport-streamable-http“Intermediaries that enforce policy based on mirrored headers … SHOULD reject the request rather than trusting unvalidated header values.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validationn/an/a
streamable-http-x-accel-buffering2026-07-28serverSHOULDtransport-streamable-http“When initiating an SSE stream, servers SHOULD include the X-Accel-Buffering: no header”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyesn/a
streamable-http-sse-comment-keepalive2026-07-28bothshould/must (lowercase)transport-streamable-http“servers are encouraged to periodically emit an SSE comment line … clients must ignore such lines and must not treat them as malformed input.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesnot verified for subscriptions/listenyes — : lines skippedInside a <Note>; lowercase.
streamable-http-modern-only-server-legacy-traffic2026-07-28serverSHOULDtransport-streamable-http“HTTP GET or DELETE to the MCP endpoint: respond with 405 Method Not Allowed. An Mcp-Session-Id header …: ignore it, and do not mint or echo session IDs. A Last-Event-ID header: ignore it”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#earlier-streamable-http-revisionsn/a as written (mcpx is dual-era, not modern-only) — but mcpx mints a session on server/discover for modern clients by choice (to bind requestState)n/aScoped to “a server that supports only this revision”.
authz-optional2025-03-26..2026-07-28bothOPTIONALauthorization“Authorization is OPTIONAL for MCP implementations.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#protocol-requirementsnot-implemented-by-choice — mcpx does no OAuthnot-implemented-by-choiceEvery other authz row applies only “When supported”.
authz-http-should-conform2025-03-26..2026-07-28bothSHOULDauthorization“Implementations using an HTTP-based transport SHOULD conform to this specification.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#protocol-requirementsnot-implemented-by-choicenot-implemented-by-choice — static headers
authz-stdio-use-environment2025-03-26..2026-07-28bothSHOULD NOTauthorization“Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#protocol-requirementsyesyes — env auth type for stdio children
authz-alt-transport-best-practice2025-03-26..2026-07-28bothMUSTauthorization“Implementations using alternative transports MUST follow established security best practices for their protocol.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#protocol-requirementsn/an/a
authz-implement-oauth212025-03-26bothMUSTauthorization“MCP auth implementations MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#overviewnot-implemented-by-choicenot-implemented-by-choice2025-06-18+: subject becomes “Authorization servers” (next row).
authz-as-implements-oauth212025-06-18..2026-07-28auth-serverMUSTauthorization“Authorization servers MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#overviewn/an/a
authz-dcr-should2025-03-26..2025-06-18bothSHOULDauthorization“MCP clients and servers SHOULD support the OAuth 2.0 Dynamic Client Registration Protocol”https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registrationnot-implemented-by-choicenot-implemented-by-choice2025-06-18 subject “MCP clients and authorization servers”. Demoted to MAY 2025-11-25 (C14).
authz-dcr-may-deprecated2025-11-25..2026-07-28client, auth-serverMAYauthorization“MCP clients and authorization servers MAY support the OAuth 2.0 Dynamic Client Registration Protocol”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#dynamic-client-registrationn/anot-implemented-by-choiceDeprecated 2026 (PR #2858).
authz-rfc8414-server-should-client-must2025-03-26bothSHOULD / MUSTauthorization“MCP servers SHOULD and MCP clients MUST implement OAuth 2.0 Authorization Server Metadata (RFC8414). Servers that do not support Authorization Server Metadata MUST follow the default URI schema.”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#overviewnot-implemented-by-choicenot-implemented-by-choiceMCP server = AS in 2025-03-26; separated 2025-06-18 (C15).
authz-grant-types2025-03-26serverSHOULDauthorization“MCP servers SHOULD support the OAuth grant types that best align with the intended audience.”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#oauth-grant-typesnot-implemented-by-choicen/a
authz-401-when-unauthorized2025-03-26serverMUSTauthorization“When authorization is required and not yet proven by the client, servers MUST respond with HTTP 401 Unauthorized.”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#example-authorization-code-grantnot-implemented-by-choicemust react to 401 — reports errorLater revs keep 401 via error table + WWW-Authenticate.
authz-base-url-discards-path2025-03-26clientMUSTauthorization“The authorization base URL MUST be determined from the MCP server URL by discarding any existing path component.”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#authorization-base-urln/anot-implemented-by-choiceRemoved 2025-06-18; later revs use path-aware well-known URIs (C15).
authz-default-endpoints-fallback2025-03-26clientMUSTauthorization“clients MUST use the following default endpoint paths … Clients MUST first attempt to discover endpoints via the metadata document before falling back”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discoverynot-implemented-by-choicenot-implemented-by-choice/authorize, /token, /register; removed 2025-06-18.
authz-bearer-header2025-03-26..2026-07-28clientMUSTauthorization“MCP client MUST use the Authorization request header field … Authorization: Bearer <access-token>”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-requirementsn/apartial — static configured bearer is sent this way; not OAuth-obtained
authz-token-every-request2025-03-26..2026-07-28clientMUSTauthorization“authorization MUST be included in every HTTP request from client to server”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-requirementsn/ayes for configured headers (POST and DELETE)2025-*: “even if they are part of the same logical session”; clause dropped 2026 (no sessions).
authz-no-token-in-query2025-03-26..2026-07-28clientMUST NOTauthorization“Access tokens MUST NOT be included in the URI query string”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-requirementsn/aconflict — mcpauth offers a query auth type; using it with a token violates this
authz-server-validates-token2025-03-26..2026-07-28serverMUSTauthorization“MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens … Invalid or expired tokens MUST receive a HTTP 401 response.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handlingnot-implemented-by-choicen/a2025-03-26: “Resource servers MUST validate”.
authz-server-validates-audience2025-06-18..2026-07-28serverMUSTauthorization“MCP servers MUST validate that access tokens were issued specifically for them as the intended audience”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handlingnot-implemented-by-choicen/aRepeated in Security Considerations (Token Audience Binding; Access Token Privilege Restriction).
authz-client-sends-only-own-as-tokens2025-06-18..2026-07-28clientMUST NOTauthorization“MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server’s authorization server.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handlingn/aoperator’s responsibility (static config per server)
authz-accept-only-own-tokens2025-06-18..2026-07-28serverMUSTauthorization“MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handlingnot-implemented-by-choice (accepts no tokens)n/a2025-06-18 said “Authorization servers MUST only accept tokens…” — subject corrected 2025-11-25 (C16).
authz-no-token-passthrough2025-06-18..2026-07-28serverMUST NOTauthorization“The MCP server MUST NOT pass through the token it received from the MCP client.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#access-token-privilege-restrictionyes — downstream request headers are not forwarded upstream; upstream credentials come from config (believed; not traced end to end)n/aKey rule for a proxy.
authz-error-status-codes2025-03-26..2026-07-28serverMUSTauthorization“Servers MUST return appropriate HTTP status codes for authorization errors: 401 … 403 … 400”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#error-handlingnot-implemented-by-choicen/a
authz-2025-03-26-security-list2025-03-26bothMUST / SHOULDauthorization“1. Clients MUST securely store tokens … 2. Servers SHOULD enforce token expiration and rotation 3. All authorization endpoints MUST be served over HTTPS 4. Servers MUST validate redirect URIs … 5. Redirect URIs MUST be either localhost URLs or HTTPS URLs”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#security-considerationsnot-implemented-by-choicenot-implemented-by-choiceLater revs move 3–5 to auth-server/client rows below.
authz-2025-03-26-implementation-reqs2025-03-26bothMUST / REQUIRED / SHOULDauthorization“1. Implementations MUST follow OAuth 2.1 security best practices 2. PKCE is REQUIRED for all clients 3. Token rotation SHOULD be implemented 4. Token lifetimes SHOULD be limited”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#implementation-requirementsnot-implemented-by-choicenot-implemented-by-choice
authz-third-party-session-binding2025-03-26serverMAY / MUSTauthorization“MCP servers MAY support delegated authorization through third-party authorization servers … MCP servers implementing third-party authorization MUST: 1. Maintain secure mapping … 2. Validate third-party token status … 3. … lifecycle management 4. Handle third-party token expiration and renewal”https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#session-binding-requirementsnot-implemented-by-choicen/aAlso “servers MUST: 1. Validate all redirect URIs 2. Securely store third-party credentials 3. … session timeout handling 4. Consider … token chaining 5. … error handling” (#security-considerations-1). Section removed 2025-06-18; replaced by token-passthrough ban.
authz-server-prm-rfc97282025-06-18..2026-07-28bothMUSTauthorization“MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728). MCP clients MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#overviewnot-implemented-by-choicenot-implemented-by-choice
authz-prm-lists-authorization-servers2025-06-18..2026-07-28serverMUSTauthorization“The Protected Resource Metadata document returned by the MCP server MUST include the authorization_servers field containing at least one authorization server.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#authorization-server-locationnot-implemented-by-choicen/a
authz-as-metadata-rfc84142025-06-18bothMUSTauthorization“Authorization servers MUST provide OAuth 2.0 Authorization Server Metadata (RFC8414). MCP clients MUST use the OAuth 2.0 Authorization Server Metadata.”https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#overviewn/anot-implemented-by-choiceRelaxed 2025-11-25 to RFC8414 or OIDC (next row).
authz-as-metadata-rfc8414-or-oidc2025-11-25..2026-07-28bothMUSTauthorization“MCP authorization servers MUST provide at least one of the following discovery mechanisms: RFC8414 … OpenID Connect Discovery 1.0. MCP clients MUST support both discovery mechanisms”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#overviewn/anot-implemented-by-choice
authz-401-www-authenticate2025-06-18serverMUSTauthorization“MCP servers MUST use the HTTP header WWW-Authenticate when returning a 401 Unauthorized to indicate the location of the resource server metadata URL”https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-locationnot-implemented-by-choicen/aRelaxed 2025-11-25 (SEP-985) to header or well-known (C17).
authz-prm-discovery-header-or-well-known2025-11-25..2026-07-28bothMUSTauthorization“MCP servers MUST implement one of the following discovery mechanisms … MCP clients MUST support both discovery mechanisms and use the resource metadata URL from the parsed WWW-Authenticate headers when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs in the order listed above.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirementsnot-implemented-by-choicenot-implemented-by-choiceOrder: path-inserted /.well-known/oauth-protected-resource/<path>, then root.
authz-client-parses-www-authenticate2025-06-18..2026-07-28clientMUSTauthorization“MCP clients MUST be able to parse WWW-Authenticate headers and respond appropriately to HTTP 401 Unauthorized responses”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirementsn/anot-implemented-by-choice (401 is surfaced as an error)
authz-client-rfc8414-discovery2025-06-18clientMUSTauthorization“MCP clients MUST follow the OAuth 2.0 Authorization Server Metadata RFC8414 specification to obtain the information required to interact with the authorization server.”https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#server-metadata-discoveryn/anot-implemented-by-choice
authz-client-as-discovery-order2025-11-25..2026-07-28clientMUSTauthorization“MCP clients MUST attempt multiple well-known endpoints … For issuer URLs with path components … clients MUST try endpoints in the following priority order”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#authorization-server-metadata-discoveryn/anot-implemented-by-choice
authz-client-validates-issuer-in-metadata2026-07-28clientMUST / MUST NOTauthorization“the issuer value in the document MUST be identical to the issuer identifier used to construct the well-known URL. If they differ, the client MUST NOT use the metadata.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#authorization-server-metadata-discoveryn/anot-implemented-by-choiceNew 2026.
authz-client-per-as-registration-state2026-07-28clientMUST / MUST NOTauthorization“Clients MUST maintain separate registration state (client credentials, tokens) per authorization server and MUST NOT assume that credentials valid for one authorization server will be accepted by another.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#authorization-server-locationn/anot-implemented-by-choice
authz-resource-indicator2025-06-18..2026-07-28clientMUSTauthorization“MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707 … The resource parameter: 1. MUST be included in both authorization requests and token requests. 2. MUST identify the MCP server … 3. MUST use the canonical URI”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#resource-parameter-implementationn/anot-implemented-by-choicePlus “MUST send this parameter regardless of whether authorization servers support it.”
authz-canonical-uri-guidance2025-06-18..2026-07-28bothSHOULDauthorization“MCP clients SHOULD provide the most specific URI … implementations SHOULD accept uppercase scheme and host components … SHOULD consistently use the form without the trailing slash”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#canonical-server-urin/an/a
authz-secure-token-storage2025-06-18..2026-07-28bothMUSTauthorization“Clients and servers MUST implement secure token storage and follow OAuth best practices”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#token-theftn/aapplies to configured secrets — mcpauth redacts in traces; storage is the config file (operator’s)
authz-as-short-lived-and-rotate2025-06-18..2026-07-28auth-serverSHOULD / MUSTauthorization“Authorization servers SHOULD issue short-lived access tokens … For public clients, authorization servers MUST rotate refresh tokens”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#token-theftn/an/a
authz-https-and-redirect-uris2025-06-18..2026-07-28bothMUSTauthorization“1. All authorization server endpoints MUST be served over HTTPS. 1. All redirect URIs MUST be either localhost or use HTTPS.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#communication-securityn/an/a
authz-pkce2025-06-18clientMUSTauthorization“MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2.”https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-code-protectionn/anot-implemented-by-choice
authz-pkce-verify-s2562025-11-25..2026-07-28client, auth-serverMUSTauthorization“MUST verify PKCE support before proceeding … MUST use the S256 code challenge method when technically capable … If code_challenge_methods_supported is absent … MCP clients MUST refuse to proceed.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#authorization-code-protectionn/anot-implemented-by-choiceAS offering OIDC Discovery “MUST include code_challenge_methods_supported”.
authz-open-redirection2025-06-18..2026-07-28client, auth-serverMUST / SHOULDauthorization“MCP clients MUST have redirect URIs registered … Authorization servers MUST validate exact redirect URIs … MCP clients SHOULD use and verify state parameters … Authorization servers MUST take precautions”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#open-redirectionn/anot-implemented-by-choice
authz-proxy-consent-per-client2025-06-18..2026-07-28serverMUSTauthorization“MCP proxy servers using static client IDs MUST obtain user consent for each dynamically registered client before forwarding to third-party authorization servers”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#confused-deputy-problemnot-implemented-by-choice — mcpx is a proxy but runs no OAuth for either siden/aDirectly addresses mcpx’s shape if it ever adds OAuth.
authz-client-registration-required2026-07-28clientMUSTauthorization“Before initiating the authorization flow, MCP clients MUST obtain a client ID through one of three registration mechanisms”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#client-registrationn/anot-implemented-by-choice
authz-registration-priority2025-11-25..2026-07-28clientSHOULDauthorization“Clients supporting all options SHOULD follow the following priority order: 1. … pre-registered … 2. … Client ID Metadata Documents … 3. … Dynamic Client Registration … 4. Prompt the user”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registrationn/anot-implemented-by-choice
authz-cimd2025-11-25..2026-07-28client, auth-serverSHOULD / MUSTauthorization“MCP clients and authorization servers SHOULD support OAuth Client ID Metadata Documents … Clients MUST host their metadata document at an HTTPS URL … The client_id URL MUST use the "https" scheme and contain a path component … MUST include at least … client_id, client_name, redirect_uris … MUST ensure the client_id value … matches”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#implementation-requirementsn/anot-implemented-by-choiceAS: MUST validate client_id match, redirect URIs, JSON structure; SHOULD fetch, cache. Clients “SHOULD check for this capability and MAY fall back”.
authz-preregistration2025-11-25..2026-07-28clientSHOULDauthorization“MCP clients SHOULD support an option for static client credentials such as those supplied by a pre-registration flow.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#pre-registrationn/an/a (no OAuth client)
authz-dcr-application-type2026-07-28clientMUST / SHOULDauthorization“MCP clients MUST specify an appropriate application_type during Dynamic Client Registration … Native applications … SHOULD use application_type: \"native\" … MCP clients MUST be prepared to handle registration failures”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#application-type-and-redirect-uri-constraintsn/anot-implemented-by-choiceSEP-837.
authz-credentials-bound-to-issuer2026-07-28clientMUST / MUST NOTauthorization“MUST associate those credentials with the specific authorization server that issued them, keyed by the authorization server’s issuer identifier … clients MUST NOT reuse client credentials from a different authorization server and MUST re-register”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#authorization-server-bindingn/anot-implemented-by-choicePre-registered mismatch: “SHOULD surface an error”.
authz-scope-in-challenge2025-11-25..2026-07-28bothSHOULD / MUST / MUST NOTauthorization“MCP servers SHOULD include a scope parameter in the WWW-Authenticate header … Clients MUST NOT assume any particular set relationship between the challenged scope set and scopes_supported. Clients MUST treat the scopes provided in the challenge as authoritative”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#scope-selection-strategynot-implemented-by-choicenot-implemented-by-choice2026 adds “When re-authorizing, clients SHOULD include these scopes alongside any previously granted scopes”.
authz-scope-selection-order2025-11-25..2026-07-28clientSHOULDauthorization“MCP clients SHOULD follow this priority order for scope selection: 1. Use scope parameter … 2. … use all scopes defined in scopes_supported”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#scope-selection-strategyn/anot-implemented-by-choice
authz-insufficient-scope-4032025-11-25..2026-07-28serverSHOULDauthorization“the server SHOULD respond with: HTTP 403 Forbidden … WWW-Authenticate header with … error=\"insufficient_scope\" … scope= … resource_metadata”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#runtime-insufficient-scope-errorsnot-implemented-by-choicen/aWhat scope contains changed (C18).
authz-all-scopes-single-challenge2026-07-28serverSHOULDauthorization“servers SHOULD include all scopes required for the current operation in a single challenge.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#runtime-insufficient-scope-errorsnot-implemented-by-choicen/a
authz-step-up2025-11-25..2026-07-28clientSHOULD / MAYauthorization“Clients SHOULD respond to these errors by requesting a new access token with an increased set of scopes … Clients SHOULD implement retry limits”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#step-up-authorization-flown/anot-implemented-by-choice2026 step 2: union of previously requested and challenged scopes (C18).
authz-server-scope-hierarchy2026-07-28serverMUSTauthorization“Servers MUST account for scope hierarchies, where a broader scope implies narrower ones”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#step-up-authorization-flownot-implemented-by-choicen/a
authz-cimd-as-security2025-11-25..2026-07-28auth-serverMUST / SHOULDauthorization“authorization servers MUST consider the security implications … SHOULD consider SSRF … SHOULD display additional warnings for localhost-only redirect URIs … MUST clearly display the redirect URI hostname”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations#client-id-metadata-document-securityn/an/a
authz-iss-validation2026-07-28client, auth-serverMUST / SHOULD / MUST NOTauthorization“the client MUST record the issuer value … authorization servers SHOULD include the iss parameter … MUST advertise … authorization_response_iss_parameter_supported … clients MUST apply the validation in RFC9207 Section 2.4 before transmitting the authorization code … MUST NOT apply scheme or host case folding … on mismatch the client MUST NOT act on or display error”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#authorization-response-validationn/anot-implemented-by-choiceSEP-2468. Table: supported=true + iss absent → reject.
authz-refresh-tokens2026-07-28client, serverMUST / SHOULD / MAY / MUST NOT / SHOULD NOTauthorization“MUST keep refresh tokens confidential … SHOULD include refresh_token in their grant_types … MAY add offline_access … MUST NOT assume refresh tokens will be issued … MCP Servers … SHOULD NOT include offline_access”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#refresh-tokensnot-implemented-by-choicenot-implemented-by-choice
authz-follow-security-considerations2025-06-18..2026-07-28bothMUSTauthorization“Implementations MUST follow OAuth 2.1 security best practices as laid out in OAuth 2.1 Section 7.”https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerationsnot-implemented-by-choicenot-implemented-by-choice2026 also: “Implementations of this specification MUST follow the normative security requirements in Security Considerations”.

Cancellation, progress, pagination, logging, completion, tasks, caching, MRTR, subscriptions

idrevssidelevelareaquoteurlmcpx-as-servermcpx-as-clientchanges
cancellation-ref-same-direction-in-progress2024-11-05..2025-11-25bothMUSTcancellation“Cancellation notifications MUST only reference requests that: Were previously issued in the same direction; Are believed to still be in-progress”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsyes — mcpx cancels only its own negative-id server→client requests on timeout (mcpserver/conn.go:263-266)yes — cancels only its own timed-out request id (mcpclient/client.go:710-716)narrowed in 2026 (next row)
cancellation-ref-client-issued-in-progress2026-07-28bothMUSTcancellation“Cancellation notifications MUST only reference requests that: Were previously issued by the client; Are believed to still be in-progress”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#behavior-requirementsyes (vacuous: sends none to modern clients)yes“same direction” → “by the client”
cancellation-client-must-not-cancel-initialize2024-11-05..2025-11-25clientMUST NOTcancellation“The initialize request MUST NOT be cancelled by clients” (schema: “A client MUST NOT attempt to cancel its initialize request.”)https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsn/ano — initialize goes through call→roundTrip, which sends notifications/cancelled on any ctx timeout (mcpclient/client.go:269, :710-716)removed in 2026 (no initialize)
cancellation-task-use-tasks-cancel2025-11-25bothMUSTcancellation“For task-augmented requests, the tasks/cancel request MUST be used instead of the notifications/cancelled notification.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsyes (mcpx never cancels a task it requested)yes (never task-augments upstream)new in 2025-11-25; gone in 2026 core
cancellation-requestid-required-non-task-forbidden-task2025-11-25bothMUST / MUST NOTcancellationschema CancelledNotificationParams.requestId: “This MUST be provided for cancelling non-task requests. This MUST NOT be used for cancelling tasks (use the tasks/cancel request instead).”https://modelcontextprotocol.io/specification/2025-11-25/schema#cancellednotificationparamsyesyesschema-only phrasing; 2024–2025-06 schemas have no such text
cancellation-receiver-should-stop-free-no-response2024-11-05..2025-11-25bothSHOULDcancellation“Receivers of cancellation notifications SHOULD: Stop processing the cancelled request; Free associated resources; Not send a response for the cancelled request”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsno — recorded only (mcpserver/server.go:1521-1538); upstream call not interrupted (docs/protocol.md §6)not-implemented — mcpclient has no handler for an inbound notifications/cancelled (legacy server cancelling its own elicitation)receiver narrowed to server in 2026
cancellation-server-should-stop-free-no-response2026-07-28serverSHOULDcancellation“Servers receiving cancellation notifications SHOULD: Stop processing…; Free associated resources; Not send a response…”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#behavior-requirementsno — as aboven/aside narrowed
cancellation-receiver-may-ignore2024-11-05..2025-11-25bothMAYcancellation“Receivers MAY ignore cancellation notifications if: The referenced request is unknown; Processing has already completed; The request cannot be cancelled”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsyesyes
cancellation-server-may-ignore2026-07-28serverMAYcancellation“Servers MAY ignore cancellation notifications if: …”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#behavior-requirementsyesn/aside narrowed
cancellation-sender-ignore-late-response2024-11-05..2025-11-25bothSHOULDcancellation“The sender of the cancellation notification SHOULD ignore any response to the request that arrives afterward”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation#behavior-requirementsyes — pending entry deleted on timeout (conn.go:249-253)yes — pending entry deleted (client.go:710-713)
cancellation-client-ignore-late-response2026-07-28clientSHOULDcancellation“The client SHOULD ignore any response to the cancelled request that arrives afterward”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#behavior-requirementsn/ayesside narrowed
cancellation-handle-races2024-11-05..2026-07-28bothMUSTcancellation“Both parties MUST handle these race conditions gracefully”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timing-considerationsyes (cancel is a map write; late cancel harmless)yes
cancellation-log-reasons2024-11-05..2026-07-28bothSHOULDcancellation“Both parties SHOULD log cancellation reasons for debugging”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#implementation-notesyes — reason kept and passed to OnCancelno evidence of logging inbound reasons
cancellation-ui-indicate2024-11-05..2026-07-28bothSHOULDcancellation“Application UIs SHOULD indicate when cancellation is requested”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#implementation-notesn/a (no UI)n/a (no UI)
cancellation-ignore-invalid2024-11-05..2026-07-28bothSHOULDcancellation“Invalid cancellation notifications SHOULD be ignored: Unknown request IDs; Already completed requests; Malformed notifications”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#error-handlingyes — malformed params dropped (server.go:1526)yes (ignored entirely)
cancellation-processing-should-cease2024-11-05..2026-07-28both (receiver)SHOULDcancellationschema: “This notification indicates that the result will be unused, so any associated processing SHOULD cease.”https://modelcontextprotocol.io/specification/2026-07-28/schema#cancellednotificationno (see stop row)not-implemented
cancellation-client-should-send2026-07-28clientSHOULDcancellation“A client SHOULD send a cancellation notification to indicate that a request it previously issued should be terminated.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellationn/ayesnew; conflicts with HTTP rule, see Conflicts C3
cancellation-server-must-send-cancelled-on-listen-teardown2026-07-28serverMUSTcancellation“A server MUST send notifications/cancelled referencing a subscriptions/listen request ID when it tears down that subscription stream”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellationno — nothing sends it; comment at server.go:623-627 claims “the transport sends the terminating notifications/cancelled”, no such code exists (only conn.go:265, which is for elicitation timeouts)n/anew; conflicts with subscriptions page, see C1
cancellation-server-only-for-listen2026-07-28serverMUST NOT / MUSTcancellation“Servers MUST NOT send notifications/cancelled for any other purpose.” + “Server-sent cancellation notifications MUST reference a subscriptions/listen request”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#behavior-requirementsyes for modern peers — conn.go:265 only fires on legacy wire requests; a modern connection never gets a server requestn/alegal→forbidden vs ≤2025-11-25, see C2
cancellation-http-disconnect-is-cancel2026-07-28serverMUSTcancellation“Streamable HTTP: Closing the SSE response stream is the cancellation signal. The server MUST treat a client disconnect as cancellation of that request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#transport-specific-cancellationno — no path from request ctx to the upstream call (docs/protocol.md §6)n/areverses ≤2025-11-25 transport rule — C4
cancellation-http-no-messages-after-close2026-07-28serverMUST NOT / SHOULDcancellation“The server SHOULD stop work on the cancelled request as soon as practical and MUST NOT send any further messages for it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellationyes (stream gone, nothing written) — stop-work SHOULD: non/a
cancellation-stdio-client-must-send2026-07-28clientMUSTcancellation“stdio: There is no per-request stream to close. The client MUST send a notifications/cancelled notification referencing the request ID.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#transport-specific-cancellationn/ayes on stdio; on legacy-era HTTP it also POSTs notifications/cancelled; on a 2026-07-28 HTTP upstream it sends none — cancelled() returns early for an HTTP transport with a meta version set (internal/mcpclient/modern.go:582) and closing the response stream is the cancellation signal, which 2026 calls the expected behaviour for that transportnew
cancellation-timeouts-establish2026-07-28bothSHOULDcancellation“Implementations SHOULD establish timeouts for all sent requests”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timeoutsyes (proto.askTimeout)yes (pool.callTimeout)new
cancellation-timeout-then-cancel2026-07-28bothSHOULDcancellation“the sender SHOULD cancel the request and stop waiting for a response”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timeoutsyes (legacy only)yesnew
cancellation-timeouts-per-request-configurable2026-07-28bothSHOULDcancellation“SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timeoutsno — global settingsno — per-server/globalnew; mcpx is “middleware”
cancellation-max-timeout-despite-progress2026-07-28bothSHOULD (+MAY)cancellation“Implementations MAY choose to reset the timeout clock when receiving a progress notification … SHOULD always enforce a maximum timeout”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timeoutsyes (fixed timeout, no reset)yesnew
progress-token-string-or-int2024-11-05..2026-07-28both (requester)MUSTprogress“Progress tokens MUST be a string or integer value”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#progress-flown/a (as a server mcpx never requests progress; a host’s token is relayed back verbatim)yes — the token mcpx sends upstream is a string, mcpx-<n> (#212)
progress-token-unique-sender2024-11-05..2025-11-25bothMUSTprogress“Progress tokens can be chosen by the sender using any means, but MUST be unique across all active requests.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#progress-flowyes (vacuous)yes (vacuous)
progress-token-unique-client2026-07-28clientMUSTprogress“Progress tokens can be chosen by the client using any means, but MUST be unique across all active requests.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#progress-flown/ayes (vacuous)sender→client
progress-receiver-may-send2024-11-05..2025-11-25bothMAYprogress“The receiver MAY then send progress notifications containing: The original progress token; The current progress value so far; An optional “total” value“https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#progress-flownot-implemented-by-choice — no notifications/progress in mcpserver; upstream progress is not forwarded downstreamn/a (never receives requests with tokens)
progress-server-may-send2026-07-28serverMAYprogress“The server MAY send progress notifications to report the status of requests the client has issued.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progressnot-implemented-by-choicen/aclient can no longer send progress — C6
progress-must-increase2024-11-05..2026-07-28both (notifier)MUSTprogress“The progress value MUST increase with each notification, even if the total is unknown.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#progress-flowyes (vacuous)n/aschema says lowercase “should increase” — C7
progress-message-human-readable2025-03-26..2026-07-28both (notifier)SHOULDprogress“The message field SHOULD provide relevant human readable progress information.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#progress-flowyes (vacuous)n/amessage added 2025-03-26
progress-only-active-tokens2024-11-05..2026-07-28both (notifier)MUSTprogress“Progress notifications MUST only reference tokens that: Were provided in an active request; Are associated with an in-progress operation”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#behavior-requirementsyes (vacuous)n/a
progress-receiver-may-skip2024-11-05..2025-11-25bothMAYprogress“Receivers of progress requests MAY: Choose not to send any progress notifications; Send notifications at whatever frequency…; Omit the total value if unknown”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#behavior-requirementsyesyes
progress-server-may-skip2026-07-28serverMAYprogress“Servers receiving a request with a progress token MAY: Choose not to send any progress notifications; …”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#behavior-requirementsyesn/aside narrowed
progress-task-token-lives-with-task2025-11-25both (receiver)MUSTprogress“the progressToken provided in the original request MUST continue to be used for progress notifications throughout the task’s lifetime, even after the CreateTaskResult has been returned”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#behavior-requirementsyes (vacuous)n/aremoved from 2026 core
progress-task-same-token2025-11-25both (receiver)MUSTprogress“Progress notifications for tasks MUST use the same progressToken that was provided in the initial task-augmented request”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#behavior-requirementsyes (vacuous)n/a
progress-task-stop-at-terminal2025-11-25both (receiver)MUSTprogress“Progress notifications for tasks MUST stop after the task reaches a terminal status (completed, failed, or cancelled)”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#behavior-requirementsyes (vacuous)n/a
progress-track-tokens2024-11-05..2026-07-28bothSHOULDprogress“Senders and receivers SHOULD track active progress tokens” (2026: “Clients and servers …”)https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#implementation-notesn/ano — OnProgress passes every token through untracked (client.go:487-493)wording only
progress-rate-limit2024-11-05..2026-07-28bothSHOULDprogress“Both parties SHOULD implement rate limiting to prevent flooding”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#implementation-notesyes (sends none)no inbound rate limiting
progress-stop-after-completion2024-11-05..2026-07-28both (notifier)MUSTprogress“Progress notifications MUST stop after completion”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#implementation-notesyes (vacuous)n/a
progress-http-notifications-relate-to-request2026-07-28serverMUSTprogress“The server MAY send JSON-RPC notifications — for example, notifications/progress or notifications/message — before the final response. These notifications MUST relate to the originating client request.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyes (vacuous)n/atransport page; listed here because it scopes progress/log delivery
pagination-no-fixed-page-size2024-11-05..2026-07-28clientMUST NOTpagination“Page size is determined by the server, and clients MUST NOT assume a fixed page size”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#pagination-modeln/ayes — loops on nextCursor (client.go:740-770, :815-825)
pagination-stable-cursors2024-11-05..2026-07-28serverSHOULDpagination“Servers SHOULD: Provide stable cursors”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinespartial — cursor is base64 o:<offset> (server.go:1499-1501); shifts if the aggregated list changes between pagesn/a
pagination-invalid-cursor-graceful2024-11-05..2026-07-28serverSHOULDpagination“Handle invalid cursors gracefully”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinesyes (silently restarts at 0)n/a
pagination-missing-nextcursor-end2024-11-05..2026-07-28clientSHOULDpagination“Clients SHOULD: Treat a missing nextCursor as the end of results”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinesn/ayes
pagination-support-both-flows2024-11-05..2026-07-28clientSHOULDpagination“Support both paginated and non-paginated flows”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinesn/ayes
pagination-cursor-opaque2024-11-05..2026-07-28clientMUSTpagination“Clients MUST treat cursors as opaque tokens: Don’t make assumptions about cursor format; Don’t attempt to parse or modify cursors”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinesn/ayes
pagination-no-persist-across-sessions2024-11-05..2025-11-25clientMUSTpagination“Don’t persist cursors across sessions”https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination#implementation-guidelinesn/ayes (cursors are loop-local)removed in 2026 (replaced by next row)
pagination-empty-cursor-not-end2026-07-28clientMUST NOTpagination“Don’t make any determination based on cursor value other than whether a non-null value was provided (e.g. an empty string is a valid cursor and thus MUST NOT be treated as the end of results)”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelinesyes (mcpx never emits "")no — if res.NextCursor == "" { break } (client.go:744, :766, :820); the string field cannot distinguish absent from ""new; legal-to-end → forbidden — C5
pagination-invalid-cursor-326022024-11-05..2026-07-28serverSHOULDpagination“Invalid cursors SHOULD result in an error with code -32602 (Invalid params).”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#error-handlingno — undecodable cursor → page 0, out-of-range → empty page (server.go:1483-1491)n/a
logging-declare-capability2024-11-05..2026-07-28serverMUSTlogging“Servers that emit log message notifications MUST declare the logging capability”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#capabilitiesyes (vacuous: emits none, declares none — server.go:1398-1403). docs/protocol.md §2.2 says logging ✓ for legacy and “removed in 2026-07-28”; both wrong vs code and vs spec (2026 deprecates, keeps logging? in schema line 808)n/a2026 adds deprecation banner
logging-client-may-setlevel2024-11-05..2025-11-25clientMAYlogging“To configure the minimum log level, clients MAY send a logging/setLevel request”https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging#setting-log-levelaccepted, {} (server.go:565-571)yes — SetLevel (client.go:574)removed in 2026 (changelog item 5)
logging-setlevel-at-or-above2024-11-05..2025-11-25servershould (schema)loggingschema: “The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message.”https://modelcontextprotocol.io/specification/2025-11-25/schema#setlevelrequestparamsyes (vacuous)n/a
logging-unset-server-chooses2024-11-05..2025-11-25serverMAYloggingschema: “If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically.”https://modelcontextprotocol.io/specification/2025-11-25/schema#loggingmessagenotificationyesn/a (mcpx receives upstream logs from any era)legal→forbidden in 2026 — C8
logging-no-message-without-loglevel2026-07-28serverMUST NOTlogging“include io.modelcontextprotocol/logLevel in the request’s _meta. The server MUST NOT emit notifications/message for a request that does not include this field.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#per-request-log-levelyes (vacuous)not-implemented — never sends logLevel upstream, yet listens for notifications/message (client.go:480-486), so from a conforming modern server it gets nonenew; schema RequestMetaObject repeats it
logging-request-scoped-only2026-07-28serverMUST NOTlogging“notifications/message is request-scoped: the server MUST NOT deliver it on a subscriptions/listen stream or on any stream other than the one carrying the response to the request that set the log level.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#per-request-log-levelyes (vacuous)n/anew
logging-may-send-before-response2026-07-28serverMAYlogging“the server MAY send notifications/message notifications at or above the requested level on the response stream of that request, before the final response.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#per-request-log-levelnot-implemented-by-choicen/anew
logging-errors-setlevel2024-11-05..2025-11-25serverSHOULDlogging“Servers SHOULD return standard JSON-RPC errors …: Invalid log level: -32602 (Invalid params); Configuration errors: -32603 (Internal error)”https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging#error-handlingno — logging/setLevel returns {} for any leveln/a
logging-invalid-loglevel-reject-request2026-07-28serverSHOULDlogging“If the io.modelcontextprotocol/logLevel value carried in a request’s _meta is not a recognized log level, the server SHOULD reject that request with a standard JSON-RPC error”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#error-handlingno — _meta.logLevel never inspectedn/anow rejects the carrying request (e.g. a tools/call)
logging-server-hygiene2024-11-05..2026-07-28serverSHOULDlogging“Servers SHOULD: Rate limit log messages; Include relevant context in data field; Use consistent logger names; Remove sensitive information”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#implementation-considerationsyes (vacuous)n/a
logging-no-secrets-pii2024-11-05..2026-07-28both (emitter)MUST NOTlogging“Log messages MUST NOT contain: Credentials or secrets; Personal identifying information; Internal system details that could aid attacks”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#securityyes (vacuous)relevant only if mcpx ever relays upstream notifications/message downstream — it does not
logging-impl-validate-control2024-11-05..2026-07-28bothSHOULDlogging“Implementations SHOULD: Rate limit messages; Validate all data fields; Control log access; Monitor for sensitive content”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#securityyes (vacuous)partial
logging-deprecated2026-07-28bothSHOULD NOT / SHOULDlogging“New implementations SHOULD NOT adopt it; existing implementations SHOULD migrate to logging to stderr for stdio transports, or to OpenTelemetry”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/loggingyes (does not adopt)n/anew (SEP-2577)
completion-declare-capability2025-03-26..2026-07-28serverMUSTcompletion“Servers that support completions MUST declare the completions capability”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#capabilitiesyes (server.go:1402)yes — asks upstream only if it declared completions (docs/protocol.md §4.3)absent in 2024-11-05 (no capability existed) — C9
completion-max-1002024-11-05..2026-07-28serverdescr. + schemacompletion“Maximum 100 items per response”; schema: “Must not exceed 100 items.” @maxItems 100https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#completion-resultsunverified — did not check truncation in s.completeshould truncate upstream values if relaying
completion-server-should2024-11-05..2026-07-28serverSHOULDcompletion“Servers SHOULD: Return suggestions sorted by relevance; Implement fuzzy matching where appropriate; Rate limit completion requests; Validate all inputs”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#implementation-considerationsyes — suggestions ranked by relevance (exact > prefix > substring tiers, rankCompletions in internal/daemon/ops.go, #2388) and completion requests rate limited per caller (completion.perMinute) and per server (completion.serverPerMinute) at the /v1 choke point (#2387); inputs validated (see completion-errors)n/aretired the stale wording that said values were matched by substring and sorted alphabetically — local matching was prefix-only (hasPrefixFold) before #2388
completion-client-should2024-11-05..2026-07-28clientSHOULDcompletion“Clients SHOULD: Debounce rapid completion requests; Cache completion results where appropriate; Handle missing or partial results gracefully”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#implementation-considerationsn/apartial — falls back to cache with "source":"cache"
completion-security2024-11-05..2026-07-28bothMUSTcompletion“Implementations MUST: Validate all completion inputs; Implement appropriate rate limiting; Control access to sensitive suggestions; Prevent completion-based information disclosure”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#securitypartial — inputs validated and rate limiting implemented per caller (completion.perMinute) and per server (completion.serverPerMinute) (#2387); control of access to sensitive suggestions and prevention of completion-based information disclosure still pending principals (#1968) / permissive-auth (#1972)n/a
completion-errors2025-03-26..2026-07-28serverSHOULDcompletion“Method not found: -32601 (Capability not supported); Invalid prompt name: -32602; Missing required arguments: -32602; Internal errors: -32603”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#error-handlingunverifiedn/aadded 2025-03-26
completion-context-arguments2025-06-18..2026-07-28clientshould (lowercase)completion“clients should include previous completions in the context.arguments object to provide context for subsequent requests.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/completion#requesting-completionsn/apass-through of caller’s paramsadded 2025-06-18
tasks-declare-capability2025-11-25both (receiver)MUSTtasks“Servers and clients that support task-augmented requests MUST declare a tasks capability during initialization.” (schema: “receivers MUST declare support for task augmentation of specific request types”)https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#capabilitiesyesnot-implemented-by-choice (mcpx declares no client tasks; upstream cannot task-augment sampling/elicitation to it)→ extension in 2026
tasks-augment-only-if-declared2025-11-25both (requestor)SHOULDtasks“Requestors SHOULD only augment requests with a task if the corresponding capability has been declared by the receiver.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#capability-negotiationyes (never augments)yes (never augments)
tasks-no-capability-no-tasks2025-11-25both (requestor)SHOULD NOTtasks“If capabilities.tasks is not defined, the peer SHOULD NOT attempt to create tasks during requests.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#capability-negotiationyesyes
tasks-tool-no-cap-no-augment2025-11-25clientMUST NOTtasks“If a server’s capabilities do not include tasks.requests.tools.call, then clients MUST NOT attempt to use task augmentation on that server’s tools”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiationn/ayes
tasks-tool-forbidden-default2025-11-25bothMUST NOT / SHOULDtasks“If execution.taskSupport is not present or "forbidden", clients MUST NOT attempt to invoke the tool as a task. Servers SHOULD return a -32601 (Method not found) error if a client attempts to do so. This is the default behavior.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiationno — mcpx publishes no execution.taskSupport on any tool (no occurrence in code), so every tool defaults to forbidden, yet it accepts task on every tools/call (server.go:481-501) instead of -32601. Net: its tasks.requests.tools.call declaration is unusable by a conforming clientn/a
tasks-tool-optional2025-11-25clientMAYtasks“If execution.taskSupport is "optional", clients MAY invoke the tool as a task or as a normal request.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiationnot-implemented (never advertises optional)n/a
tasks-tool-required2025-11-25bothMUSTtasks“If execution.taskSupport is "required", clients MUST invoke the tool as a task. Servers MUST return a -32601 (Method not found) error if a client does not attempt to do so.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiationn/a (never required)n/a — mcpx reads upstream execution.taskSupport to serve its own clients, but never task-augments the upstream call; a required upstream tool that enforces it would failconflicts with -32600 MAY — C10
tasks-requestor-may-ttl2025-11-25both (requestor)MAYtasks“Requestors MAY include a ttl value indicating the desired task lifetime duration (in milliseconds) since its creation.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#creating-taskshonoured (wantsTask)n/a
tasks-poll-respect-interval2025-11-25both (requestor)SHOULDtasks“Requestors SHOULD respect the pollInterval provided in responses when determining polling frequency.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#getting-tasksn/an/a (no upstream tasks)
tasks-poll-until-terminal2025-11-25both (requestor)SHOULDtasks“Requestors SHOULD continue polling until the task reaches a terminal status … or until encountering the input_required status.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#getting-tasksn/an/a
tasks-sse-client-may-disconnect2025-11-25clientMAY / MUSTtasks“clients MAY disconnect from an SSE stream opened by the server in response to a tasks/get request at any time … all implementations MUST continue to comply with the existing Streamable HTTP transport specification.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#getting-tasksyes (tasks/get answered as JSON)n/asame note on tasks/result
tasks-status-notification-optional2025-11-25bothMAY / MUST NOTtasks“Receivers MAY send notifications/tasks/status notifications when a task’s status changes.” “Requestors MUST NOT rely on receiving the notifications/tasks/status notification”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-notificationsnot-implemented-by-choice (sends none)yes — dropped, polls (docs/protocol.md §4.2)
tasks-status-notification-no-related-task2025-11-25both (receiver)SHOULD NOTtasks“the notifications/tasks/status notification SHOULD NOT include the io.modelcontextprotocol/related-task metadata”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-notificationsyes (vacuous)n/a
tasks-receiver-without-cap-ignore2025-11-25both (receiver)MUSTtasks“Receivers that do not declare the task capability for a request type MUST process requests of that type normally, ignoring any task-augmentation metadata if present.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-support-and-handlingyes — task honoured only on tools/calln/a — mcpx declares no client task caps; unverified whether it strips task from an upstream sampling/createMessage
tasks-receiver-may-require2025-11-25both (receiver)MAYtasks“Receivers that declare the task capability for a request type MAY return an error for non-task-augmented requests” / “-32600 (Invalid request)”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-support-and-handlingnot usedn/a-32600 here vs -32601 for tools — C10
tasks-id-string-receiver-unique2025-11-25both (receiver)MUSTtasks“Task IDs MUST be a string value.” “…MUST be generated by the receiver…” “…MUST be unique among all tasks controlled by the receiver.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-id-requirementsyes — tsk- + 16 hex from crypto/rand (tasks/tasks.go:86-90)n/a
tasks-begin-working2025-11-25both (receiver)MUSTtasks“Tasks MUST begin in the working status when created.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-status-lifecycleyesn/a
tasks-valid-transitions2025-11-25both (receiver)MUSTtasks“Receivers MUST only transition tasks through the following valid paths”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-status-lifecycleyes — SetStatus is a no-op on terminal; not otherwise validated (tasks.go:185-191)n/a
tasks-terminal-immutable2025-11-25both (receiver)MUST NOTtasks“Tasks with a completed, failed, or cancelled status are in a terminal state and MUST NOT transition to any other status”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-status-lifecycleyes (tasks.go:122-124, :201)n/aextension says cancel may still end non-cancelled — C11
tasks-input-required-move2025-11-25both (receiver)SHOULDtasks“When the task receiver has messages for the requestor that are necessary to complete the task, the receiver SHOULD move the task to the input_required status.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusyes (SetTaskStatus)n/a
tasks-input-request-related-task2025-11-25both (receiver)MUSTtasks“The receiver MUST include the io.modelcontextprotocol/related-task metadata in the request to associate it with the task.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusno — no related-task anywhere in mcpx; an elicitation/create raised by a task-run tools/call goes out untaggedn/a
tasks-requestor-input-required-call-result2025-11-25both (requestor)SHOULDtasks“When the requestor encounters the input_required status, it SHOULD preemptively call tasks/result.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusn/an/a
tasks-leave-input-required2025-11-25both (receiver)SHOULDtasks“When the receiver receives all required input, the task SHOULD transition out of input_required status”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusunverifiedn/a
tasks-sse-any-stream2025-11-25clientSHOULDtasks“clients SHOULD expect messages to be delivered on any SSE stream, including the HTTP GET stream.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusn/an/a
tasks-sse-no-upgrade-get2025-11-25serverSHOULD NOTtasks“Where possible, servers SHOULD NOT upgrade to an SSE stream in response to a tasks/get request”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#input-required-statusyesn/a
tasks-timestamps2025-11-25both (receiver)MUSTtasks“Receivers MUST include a createdAt … timestamp in all task responses” / “…MUST include a lastUpdatedAt …”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#ttl-and-resource-managementyes (tasks.go:30-31)n/a
tasks-ttl-override-report2025-11-25both (receiver)MAY / MUSTtasks“Receivers MAY override the requested ttl duration.” “Receivers MUST include the actual ttl duration (or null for unlimited) in tasks/get responses.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#ttl-and-resource-managementyesn/a
tasks-delete-after-ttl2025-11-25both (receiver)MAYtasks“After a task’s ttl lifetime has elapsed, receivers MAY delete the task and its results, regardless of the task status.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#ttl-and-resource-managementyes (tasks.go:148-153, also cancels the work)n/a
tasks-poll-interval2025-11-25bothMAY / SHOULDtasks“Receivers MAY include a pollInterval value … Requestors SHOULD respect this value when provided.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#ttl-and-resource-managementyes (1000 ms)n/a
tasks-return-createtaskresult2025-11-25both (receiver)MUST / SHOULDtasks“Receivers that accept a task-augmented request MUST return a CreateTaskResult as the response. This result SHOULD be returned as soon as possible after accepting the task.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#result-retrievalyes ({"task": …}, server.go:500)n/a
tasks-result-terminal-final2025-11-25both (receiver)MUSTtasks“…for a task in a terminal status … it MUST return the final result of the underlying request, whether that is a successful result or a JSON-RPC error.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#result-retrievalyes (mcpserver/tasks.go:104-118)n/a
tasks-result-blocks2025-11-25both (receiver)MUSTtasks“…in any other non-terminal status (working or input_required), it MUST block the response until the task reaches a terminal status.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#result-retrievalyes (st.Result(ctx,…))n/aremoved in extension (no tasks/result)
tasks-result-exact2025-11-25both (receiver)MUSTtasks“receivers MUST return from tasks/result exactly what the underlying request would have returned”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#result-retrievalyes; except a cancelled task’s tasks/result returns whatever the aborted goroutine yielded — unverifiedn/a
tasks-related-task-all-messages2025-11-25bothMUSTtasks“All requests, notifications, and responses related to a task MUST include the io.modelcontextprotocol/related-task key in their _meta field”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#associating-task-related-messagesnon/ainternally contradicted — C12
tasks-taskid-param-authoritative2025-11-25bothMUST / SHOULD NOTtasks“the taskId parameter in the request MUST be used as the source of truth … Requestors SHOULD NOT include io.modelcontextprotocol/related-task metadata in these requests, and receivers MUST ignore such metadata”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#associating-task-related-messagesyesn/a
tasks-no-related-task-in-get-list-cancel-results2025-11-25both (receiver)SHOULD NOTtasks“for the tasks/get, tasks/list, and tasks/cancel operations, receivers SHOULD NOT include io.modelcontextprotocol/related-task metadata in the result messages”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#associating-task-related-messagesyesn/a
tasks-result-response-related-task2025-11-25both (receiver)MUSTtasks“The tasks/result operation MUST include this metadata in its response, as the result structure itself does not contain the task ID.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#related-task-metadatano — reply(result) untagged (mcpserver/tasks.go:118)n/a
tasks-list-paginate2025-11-25both (receiver)SHOULD / MUSTtasks“Receivers SHOULD use cursor-based pagination…” “Receivers MUST include a nextCursor in the response if more tasks are available.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-listingyes (mcpserver/tasks.go:89-95)n/atasks/list removed in extension
tasks-list-cursor-opaque2025-11-25both (requestor)MUSTtasks“Requestors MUST treat cursors as opaque tokens and not attempt to parse or modify them.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-listingn/an/a
tasks-get-implies-list2025-11-25both (receiver)MUSTtasks“If a task is retrievable via tasks/get for a requestor, it MUST be retrievable via tasks/list for that requestor.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-listingyes (one global store)n/a
tasks-cancel-terminal-reject2025-11-25both (receiver)MUSTtasks“Receivers MUST reject cancellation requests for tasks already in a terminal status … with error code -32602 (Invalid params).”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-cancellationno — Cancel returns the terminal snapshot as success (tasks/tasks.go:194-206, mcpserver/tasks.go:120-125)n/a
tasks-cancel-transition-before-response2025-11-25both (receiver)SHOULD / MUSTtasks“receivers SHOULD attempt to stop the task execution and MUST transition the task to cancelled status before sending the response.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-cancellationyesn/aextension: cooperative, not obliged — C11
tasks-cancelled-sticky2025-11-25both (receiver)MUSTtasks“Once a task is cancelled, it MUST remain in cancelled status even if execution continues to completion or fails.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-cancellationyes (tasks.go:122-124)n/areversed by extension — C11
tasks-cancelled-may-delete2025-11-25bothMAY / SHOULD NOTtasks“receivers MAY delete cancelled tasks at their discretion at any time” “Requestors SHOULD NOT rely on cancelled tasks being retained”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-cancellationyesn/a
tasks-protocol-errors2025-11-25both (receiver)MUSTtasks“Receivers MUST return standard JSON-RPC errors …: Invalid or nonexistent taskId …: -32602; Invalid or nonexistent cursor in tasks/list: -32602; Attempt to cancel a task already in a terminal status: -32602; Internal errors: -32603”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#protocol-errorspartial — unknown taskId → -32602 yes; bad cursor → silently page 0 (no); terminal cancel → success (no)n/a
tasks-informative-errors2025-11-25both (receiver)SHOULDtasks“Receivers SHOULD provide informative error messages to describe the cause of errors.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#protocol-errorsyes (“no task X; it may have expired”)n/a
tasks-failed-status-message2025-11-25both (receiver)SHOULDtasks“The tasks/get response SHOULD include a statusMessage field with diagnostic information about the failure.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-execution-errorspartial — set for JSON-RPC faults, empty for isError tool results (tasks.go:127-139)n/a
tasks-result-same-error2025-11-25both (receiver)MUSTtasks“If the underlying request resulted in a JSON-RPC error, tasks/result MUST return that same JSON-RPC error.” / “…MUST return a successful JSON-RPC response containing that result.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-execution-errorsyesn/a
tasks-bind-auth-context2025-11-25both (receiver)MUSTtasks“When an authorization context is provided, receivers MUST bind tasks to said context.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controlnot-implemented-by-choice — mcpx has no authorization context (no OAuth)n/a
tasks-document-unbound2025-11-25both (receiver)SHOULDtasks“receivers SHOULD document this limitation clearly, as task results may be accessible to any requestor that can guess the task ID.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controlnot found in docs/protocol.mdn/a
tasks-unbound-secure-ids2025-11-25both (receiver)MUSTtasks“If context-binding is unavailable, receivers MUST generate cryptographically secure task IDs with enough entropy to prevent guessing”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controlborderline — CSPRNG yes, 64 bits; “enough” undefinedn/a
tasks-unidentified-no-list2025-11-25both (receiver)SHOULD NOTtasks“receivers that cannot identify requestors SHOULD NOT declare the tasks.list capability”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controlno — declares tasks.list (server.go:1411-1412) and tasks/list returns every task in the daemon across all connectionsn/a
tasks-bound-reject-foreign2025-11-25both (receiver)MUSTtasks“If context-binding is available, receivers MUST reject tasks/get, tasks/result, and tasks/cancel requests for tasks that do not belong to the same authorization context…”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controln/a (no context)n/a
tasks-rate-limit2025-11-25both (receiver)SHOULDtasks“receivers SHOULD implement rate limiting on task operations to prevent denial-of-service and enumeration attacks.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-isolation-and-access-controlnon/a
tasks-resource-limits2025-11-25both (receiver)SHOULDtasks“Enforce limits on concurrent tasks per requestor; Enforce maximum ttl durations …; Clean up expired tasks promptly; Document maximum supported ttl…”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#resource-managementpartial — expiry yes; no max ttl clamp found, no concurrency capn/a
tasks-audit2025-11-25bothSHOULDtasks“Receivers SHOULD: Log task creation, completion, and retrieval events…” / “Requestors SHOULD: Log task lifecycle events…”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#audit-and-loggingunverifiedn/a
ext-tasks-client-declare-per-request2026-07-28 (ext)clientmust (informal)tasks“The client includes io.modelcontextprotocol/tasks in its per-request capabilities.”docs/extensions/tasks/overview.mdx#how-tasks-workn/anot-implemented-by-choicereplaces 2025-11-25 initialize capability
ext-tasks-server-advertise-discover2026-07-28 (ext)servermust (informal)tasks“Include the extension in your server/discover capabilities”docs/extensions/tasks/overview.mdx#for-mcp-serversyes (extensions["io.modelcontextprotocol/tasks"], server.go:1415-1417)n/a
ext-tasks-never-unsolicited2026-07-28 (ext)servermust (informal)tasks“Before returning a CreateTaskResult, verify that the client included the extension in its per-request capabilities. Never return a task to a client that did not declare support.”docs/extensions/tasks/overview.mdx#for-mcp-serverspartial — returns a task only when the request carried a task param; never checks the extension capabilityn/achangelog wording “unsolicited” — C13
ext-tasks-server-decides2026-07-28 (ext)serverdescr.tasks“The server decides per-request whether to create a task. Clients opt in once via the extension capability … No per-tool warmup or per-request flag.”docs/extensions/tasks/overview.mdx#why-not-just-blockno — mcpx uses the 2025-11-25 per-request task flag, which the extension removedn/amodel reversed vs 2025-11-25
ext-tasks-durable-before-response2026-07-28 (ext)servermust (informal)tasks“The task must be durably created before sending the response.”docs/extensions/tasks/overview.mdx#for-mcp-serversno in the crash sense — in-memory per-daemon store (tasks/tasks.go:73-76)n/a
ext-tasks-createtaskresult-shape2026-07-28 (ext)servermust (informal)tasks“respond with resultType: "task" and a Task object containing a unique taskId, initial status, ttlMs, and pollIntervalMs.”docs/extensions/tasks/overview.mdx#for-mcp-serversno — {"task":{…,"ttl","pollInterval"}} + resultType:"complete" stamped by server.go:452-453n/afield renames vs 2025-11-25
ext-tasks-get-carries-result2026-07-28 (ext)servermust (informal)tasks“Return the current task state on each poll. For terminal states, include the result (on completed) or error (on failed) field.”docs/extensions/tasks/overview.mdx#for-mcp-serversno — result only via tasks/resultn/atasks/result removed (changelog 6)
ext-tasks-get-input-requests2026-07-28 (ext)serverdescr.tasks“If the task moves to input_required, the tasks/get response includes an inputRequests map”docs/extensions/tasks/overview.mdx#how-tasks-worknon/a
ext-tasks-update2026-07-28 (ext)servermust (informal)tasks“Accept inputResponses keyed to outstanding inputRequests. Acknowledge with an empty result. Ignore responses for unknown or already-satisfied keys.”docs/extensions/tasks/overview.mdx#for-mcp-serversnot-implemented (tasks/update absent → -32601)n/anew method
ext-tasks-cancel-cooperative2026-07-28 (ext)servermust (informal)tasks“Acknowledge cancellation requests with an empty result. Honor them when possible, but cancellation is cooperative — the task may still reach a non-cancelled terminal status.”docs/extensions/tasks/overview.mdx#for-mcp-serversno — returns Task snapshot, forces cancelledn/areverses 2025-11-25 — C11
ext-tasks-client-polymorphic2026-07-28 (ext)clientmust (informal)tasks“be prepared to receive either the standard result or a CreateTaskResult with resultType: "task".”docs/extensions/tasks/overview.mdx#for-mcp-clientsn/anot-implemented-by-choice (does not declare the extension, so should never receive one)
ext-tasks-client-poll2026-07-28 (ext)clientmust (informal)tasks“Call tasks/get with the returned taskId, respecting the pollIntervalMs value. Continue polling until the task reaches a terminal status”docs/extensions/tasks/overview.mdx#for-mcp-clientsn/anot-implemented-by-choice
ext-tasks-client-persist-ids2026-07-28 (ext)clientmust (informal)tasks“Store task IDs durably so polling can resume after a client crash or restart.”docs/extensions/tasks/overview.mdx#for-mcp-clientsn/anot-implemented-by-choice
ext-tasks-notifications-via-listen2026-07-28 (ext)bothdescr. (MAY)tasks“Servers can push status updates via notifications/tasks. Clients opt into these through the subscriptions/listen mechanism.”docs/extensions/tasks/overview.mdx#notificationsyes — taskIds agreed for known visible tasks; notifications/tasks carries the DetailedTask (mcpserver/listen.go watchTasks)not-implementednotifications/tasks/status → notifications/tasks; not in SubscriptionFilter of core schema
caching-server-must-hint2026-07-28serverMUST*caching“Servers MUST include caching hints on results with resultType: "complete" returned by the following operations: server/discover, tools/list, prompts/list, resources/list, resources/templates/list, resources/read” (schema: ttlMs, cacheScope required on CacheableResult)https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-resultsno — ttlMs/cacheScope appear nowhere in mcpxn/anew; changelog item 5 omits server/discover — C14
caching-input-required-no-hints2026-07-28serverdescr.caching“Interim results with resultType: "input_required" … are not cacheable and carry no caching hints.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-resultsyesn/a
caching-key-method-params2026-07-28clientMUST NOTcaching“Clients MUST NOT serve a cached response for a request whose method or parameters differ from the request that produced it.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cache-keyn/ayes as far as seen (per-server list caches, invalidated on list_changed)
caching-no-cache-mrtr-retries2026-07-28clientMUST NOTcaching“requests carrying inputResponses or requestState—MUST NOT be cached”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cache-keyn/ayes (only lists cached; resources/read retries not cached — unverified)
caching-ttl-zero-stale2026-07-28clientSHOULDcaching“If ttlMs is 0, the response SHOULD be considered immediately stale.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#time-to-live-ttl-fieldn/ano — ttlMs ignored; list cache lives until list_changed
caching-ttl-positive-fresh2026-07-28clientSHOULDcaching“If ttlMs is positive, the client SHOULD consider the result fresh for that many milliseconds after receiving the response.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#time-to-live-ttl-fieldn/ano
caching-ttl-absent-zero2026-07-28clientSHOULDcaching“If ttlMs is absent, clients SHOULD assume a default of 0 (immediately stale)”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#time-to-live-ttl-fieldn/ano — legacy upstream lists are cached indefinitely
caching-ttl-negative-zero2026-07-28clientSHOULDcaching“If ttlMs is negative, clients SHOULD ignore it and treat it as 0.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#time-to-live-ttl-fieldn/ano
caching-ttl-non-negative2026-07-28serverMUSTcaching“Servers MUST provide a ttlMs value that is >= 0.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#time-to-live-ttl-fieldno (absent)n/a
caching-stale-refetch2026-07-28clientSHOULDcaching“Once the TTL expires, the response is stale and the client SHOULD re-fetch on next access.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#freshness-calculationn/ano
caching-ttl-not-poll2026-07-28clientSHOULD NOT / MUSTcaching“Clients SHOULD NOT treat TTL as a polling interval… Implementations that do choose to poll MUST apply jitter and backoff.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#freshness-calculationn/ayes (no TTL polling)
caching-may-refetch-early-serve-stale2026-07-28clientMAYcaching“Clients MAY re-fetch before the TTL expires…” “Clients MAY serve stale responses if errors occur during re-fetching”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#freshness-calculationn/ayes (serves cache on error)
caching-public-shareable2026-07-28bothMAYcaching“"public" … Any client, shared gateway, or caching proxy MAY store and serve the cached response to any user.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cache-scope-fieldn/arelevant — mcpx is a “shared gateway”
caching-private-no-cross-auth2026-07-28both (any cache)MUST NOTcaching“Caches MUST NOT be shared across authorization contexts (e.g. a different access token requires a different cache).”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cache-scope-fieldn/aat risk — mcpx ignores cacheScope and re-serves an upstream’s cached lists/reads to every downstream client; safe only while each upstream sees one mcpx credential and every downstream is the same user
caching-notification-invalidates2026-07-28clientdescr. (lowercase should)caching“the notification invalidates the cached response and it should be considered immediately stale.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#interaction-with-notificationsn/ayes (client.go:494-499 invalidate) — but modern upstreams only notify over subscriptions/listen, which mcpx never opens
caching-page-refetch2026-07-28clientSHOULDcaching“When a cached page expires, the client SHOULD re-fetch that page using its cursor.” / “…SHOULD re-fetch from the beginning (without a cursor).” / “If a cursor becomes invalid … SHOULD discard all cached pages and re-fetch from the beginning.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#interaction-with-paginationn/apartial — always re-fetches whole list
caching-page-same-scope2026-07-28serverMUSTcaching“Servers MUST apply the same cacheScope to all response pages for a given list request.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#interaction-with-paginationno (absent)n/a
caching-server-access-control2026-07-28serverMUST* / MUST NOT*caching“Servers MUST be aware that responses with a "public" cacheScope may be shared…” “MUST apply appropriate per-primitive access controls, and MUST NOT rely on cacheScope alone to prevent unauthorized access to primitives.”https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#security-considerationsn/a until hints are emitted; mcpx has no per-primitive access control (see authorization area)n/a
mrtr-server-requests-via-mrtr2026-07-28serverMUSTmrtr“Servers MUST send server-to-client requests (such as roots/list, sampling/createMessage, or elicitation/create) using the MRTR pattern. The previous pattern of server-initiated requests is no longer supported.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtryes for modern clients (mcpserver/ask.go:262); legacy clients get wire requests, correctlyn/alegal→forbidden — C2
mrtr-requeststate-opaque-client2026-07-28clientMUST NOTmrtr“Clients MUST NOT inspect, parse, modify, or make any assumptions about its contents.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#inputrequiredresultn/ayes (mcpclient/modern.go:144-186 copies bytes)
mrtr-supported-requests-only2026-07-28serverMAY / MUST NOTmrtr“Servers MAY send InputRequiredResult responses on the following client requests: prompts/get, resources/read, tools/call” “Servers MUST NOT send InputRequiredResult responses on any other client requests.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#supported-requestsyes (only tools/call)should reject/ignore on others — unverified
mrtr-inputrequests-keys-values2026-07-28serverMUSTmrtr“inputRequests keys … MUST be unique within the scope of the request.” “values are request objects that MUST be one of ElicitRequest, CreateMessageRequest, or ListRootsRequest”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyesn/a
mrtr-requeststate-attacker-controlled2026-07-28serverMUSTmrtr“servers MUST treat requestState as an attacker-controlled input. If requestState influences authorization, resource access, or business logic, servers MUST protect its integrity (e.g. HMAC or AEAD) and MUST reject state that fails verification.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyes — HMAC-SHA256, per-process key (mcpserver/state.go)n/a
mrtr-requeststate-replay-fields2026-07-28serverSHOULDmrtr“servers SHOULD include … the authenticated principal …; a short expiry (TTL) …; an identifier for the originating request, e.g. the method name and a digest of its salient parameters”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowpartial — connection id (no principal exists), expiry, call id; no method/params digestn/a
mrtr-single-use-enforced2026-07-28serverMUSTmrtr“Servers for which a given requestState must be consumed at most once … MUST enforce that invariant server-side.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowunverified whether a resumed call id can be resumed twicen/a
mrtr-at-least-one-field2026-07-28serverMUSTmrtr“Servers MUST include at least one of inputRequests or requestState in every InputRequiredResult response.” (schema same)https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyesn/a
mrtr-only-declared-capabilities2026-07-28serverMUST NOTmrtr“Servers MUST NOT send an inputRequests that the client has not declared support for in its capabilities.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyes (per-request caps, docs/protocol.md §3.1)n/a
mrtr-no-assume-retry2026-07-28serverMUST NOT / MAYmrtr“Servers MUST NOT assume that clients will fulfill the inputRequests or retry the original request. Servers MAY choose to return an InputRequiredResult on multiple attempts”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyes (proto.askTTL, askRounds)n/a
mrtr-client-fulfil-before-retry2026-07-28clientMUST / MAYmrtr“the client MUST construct the requested inputs before retrying the original request. If the InputRequiredResult does not contain the inputRequests field, the client MAY retry the original request immediately.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflown/ayes
mrtr-client-echo-state2026-07-28clientMUST / MUST NOTmrtr“the client MUST echo back the exact value of that field when retrying … If the InputRequiredResult does not contain a requestState field, the client MUST NOT include one in the retry.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflown/ayes (modern.go:172-186)
mrtr-new-id-on-retry2026-07-28clientMUSTmrtr“The JSON-RPC id MUST be different between the initial request and the retry”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflown/ayes (fresh id per round trip)
mrtr-no-cross-request-use2026-07-28clientMUST NOTmrtr“They MUST NOT be used for any other request that the client may be sending in parallel.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflown/ayes
mrtr-server-validate-responses2026-07-28serverSHOULDmrtr“Servers SHOULD validate that the data provided by the client is a valid InputResponses object” / “…SHOULD ignore any information it does not recognize” / “…SHOULD respond with a new InputRequiredResult requesting the missing information again, rather than returning an error.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#error-handlingunverifiedn/a
subscriptions-list-changed-should-notify2024-11-05..2025-11-25serverSHOULDsubscriptions“When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification” (same for tools, prompts)https://modelcontextprotocol.io/specification/2025-11-25/server/resources#list-changed-notificationpartial — declared only when the connection can push (server.go:1389-1403); unverified that a legacy connection receives list_changed without a listen stream (restartListen is only called from listen/subscribe)yes — consumes and invalidatesdelivered unsolicited ≤2025-11-25; only via listen in 2026
subscriptions-list-changed-via-listen2026-07-28serverSHOULDsubscriptions“capability SHOULD send a notification to clients that have opened a subscriptions/listen stream with…”https://modelcontextprotocol.io/specification/2026-07-28/server/tools#list-changed-notificationpartial (envelope wrong, see below)not-implemented — mcpx never opens subscriptions/listen upstream
subscriptions-resources-updated-only-if-subscribed2024-11-05..2025-11-25servershould (schema)subscriptionsschema: “This should only be sent if the client previously sent a resources/subscribe request.”https://modelcontextprotocol.io/specification/2025-11-25/schema#resourceupdatednotificationyesyes — sends resources/subscribe (client.go:532-542)resources/subscribe removed 2026 (changelog 4)
subscriptions-only-requested-types2026-07-28serverMUST NOTsubscriptions“The server MUST NOT send notification types the client has not explicitly requested.” (schema SubscriptionFilter, SubscriptionsListenRequestParams same)https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#opening-a-streamyes (cli/serve.go:726-742) — but legacy resources/subscribe calls restartListen with a URI-only filter, dropping any list-changed kinds the same connection hadn/a
subscriptions-ack-first-with-id2026-07-28serverMUST / MUST NOTsubscriptions“The server MUST send notifications/subscriptions/acknowledged as the first message carrying the subscription’s ID in _meta under io.modelcontextprotocol/subscriptionId, and MUST NOT send any notification on the subscription before it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#acknowledgmentno — id placed in params.subscriptionId, not _meta (server.go:637-639); ack is pushed after restartListen starts the goroutine, so a first event can race ahead of itn/anew
subscriptions-stdio-ack-order-per-id2026-07-28serverMAYsubscriptions“On stdio … this ordering is defined per subscription ID and not per channel: messages belonging to other subscriptions MAY be interleaved before it.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#acknowledgmentn/an/a
subscriptions-ack-reports-agreed-subset2026-07-28serverdescr. (schema-required field)subscriptions“The notifications field in the acknowledgment reflects the subset the server agreed to honor. Notification types the server does not support are omitted.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#acknowledgmentno — notifications (required in SubscriptionsAcknowledgedNotificationParams) is omittedn/a
subscriptions-client-check-ack2026-07-28clientSHOULDsubscriptions“The client SHOULD check the acknowledged filter against what it requested and handle any unsupported types gracefully.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#acknowledgmentn/anot-implemented
subscriptions-tag-every-notification2026-07-28serverMUSTsubscriptionsbasic/index: “On notifications delivered via a subscriptions/listen stream, the server MUST include io.modelcontextprotocol/subscriptionId in _meta”; schema NotificationMetaObject: “server MUST include this key on every notification delivered via a subscriptions/listen stream”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metano — no code writes io.modelcontextprotocol/subscriptionIdn/asubscriptions page itself only says “carry” (no keyword)
subscriptions-stdio-client-correlate2026-07-28clientMUSTsubscriptions“On stdio, where all messages share a single channel, clients MUST use this field to correlate notifications with their originating subscription.” (stdio.mdx repeats)https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#receiving-notificationsn/anot-implemented (no listen upstream)
subscriptions-multiple-concurrent2026-07-28clientMAYsubscriptions“A client MAY have multiple active subscriptions concurrently … Each subscription is identified by the JSON-RPC request ID of its subscriptions/listen request”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#multiple-concurrent-subscriptionsno — one stream per Conn; a second listen silently replaces the first (server.go:1421-1444), and the first listen request is never answered or cancelledn/a
subscriptions-server-teardown-respond2026-07-28serverSHOULDsubscriptions“The server tears it down (e.g., during shutdown) — it SHOULD send a successful subscriptions/listen response to signal a graceful end … then close the stream.” / Graceful Closure: “it SHOULD respond to the original subscriptions/listen request with a completion result before closing the stream.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#graceful-closureno — listen handler returns nil and nothing ever answers it (server.go:640)n/acontradicts cancellation MUST — C1
subscriptions-listen-result-meta2026-07-28serverschema-requiredsubscriptionsschema SubscriptionsListenResult { _meta: SubscriptionsListenResultMetaObject } — “io.modelcontextprotocol/subscriptionId: RequestId” (required), “equals this response’s id”https://modelcontextprotocol.io/specification/2026-07-28/schema#subscriptionslistenresultno (no response)n/a
subscriptions-client-reconnect-on-drop2026-07-28clientMAYsubscriptions“a transport that closes without it indicates an unexpected disconnect, which the client MAY treat as a trigger to reconnect.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#graceful-closuren/an/a
subscriptions-stdio-resubscribe2026-07-28clientMUSTsubscriptions“On stdio, if the connection is terminated and then re-established, the client MUST re-send subscriptions/listen … — the server holds no subscription state across reconnections.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#graceful-closureyes (state dies with Conn)n/a
subscriptions-request-scoped-not-on-listen2026-07-28serverdescr. (bold “not”)subscriptions“Request-scoped notifications like notifications/progress and notifications/message are not delivered on the listen stream — they flow only on the response stream of the request they relate to.”https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messagesyes (vacuous)n/a

Tools, resources, prompts, discover

idrevssidelevelareaquoteurlmcpx-as-servermcpx-as-clientchanges
tools-human-in-loop-deny2024-11-05 – 2026-07-28clientSHOULDtools“there SHOULD always be a human in the loop with the ability to deny tool invocations”2026-07-28/server/tools#user-interaction-modelno — host-application duty; mcpx is not the hostnot-implemented-by-choice — mcpx is not the UI; elicitation broker is the only human channel
tools-ui-expose-indicate-confirm2024-11-05 – 2026-07-28clientSHOULDtools“Applications SHOULD: Provide UI that makes clear which tools are being exposed… Insert clear visual indicators… Present confirmation prompts”2026-07-28/server/tools#user-interaction-modelno — host dutynot-implemented-by-choice — no UI in the proxy path
tools-declare-capability2024-11-05 – 2026-07-28serverMUSTtools“Servers that support tools MUST declare the tools capability”2026-07-28/server/tools#capabilitiesyes — declared all revs (protocol.md §2.2)no (server obligation); mcpx relies on itwhere declared changes: initialize (legacy) vs DiscoverResult (2026); tools page doesn’t say where
tools-list-changed-means-emit2024-11-05 – 2026-07-28server(definition)tools“listChanged indicates whether the server will emit notifications when the list of available tools changes”2026-07-28/server/tools#capabilitiesyes — declared only when a notifier exists (§2.2)yes — should only expect notifications when declared
tools-list-changed-notify2024-11-05 – 2025-11-25serverSHOULDtools“servers that declared the listChanged capability SHOULD send a notification”2025-11-25/server/tools#list-changed-notificationyesnochanged in 2026 (only to listen streams)
tools-list-changed-notify-to-listeners2026-07-28serverSHOULDtools“SHOULD send a notification to clients that have opened a subscriptions/listen stream with toolsListChanged: true”2026-07-28/server/tools#list-changed-notificationyes — “only the four notification kinds the filter names”yes — must open listen with toolsListChanged to receiveCHANGED: legacy sends on session; 2026 only on opted-in stream
tools-list-must-respond-when-declared2026-07-28serverMUSTtools“Servers that declare the tools capability MUST respond to tools/list requests with the set of tools currently available to the requesting client”2026-07-28/server/tools#capabilitiesyesnonew in 2026 (implicit before)
tools-list-not-per-connection2026-07-28serverMUST NOTtools“MUST NOT vary per-connection or as a side effect of other requests on the connection”2026-07-28/server/tools#capabilitiesyes — must hold; check: mcpx Conn state (subscriptions, caps) must not filter the listnoCHANGED: legacy servers could legitimately vary per session
tools-list-may-vary-by-auth2026-07-28serverMAYtools“The set MAY vary by the authorization presented on the request”2026-07-28/server/tools#capabilitiesnot-implemented-by-choice — mcpx does no authnonew
tools-list-may-be-empty-change2026-07-28serverMAYtools“This set MAY be empty and MAY change over time”2026-07-28/server/tools#capabilitiesyesyes — tolerate empty lists
tools-list-deterministic-order2026-07-28serverSHOULDtools“Servers SHOULD return tools in a deterministic order”2026-07-28/server/tools#capabilitiesyes — must verify s.Tools() ordering is stable (pagination via opaque cursor depends on it anyway)nonew (changelog “Minor” #3)
tools-annotations-untrusted2025-03-26 – 2026-07-28clientMUSTtools“clients MUST consider tool annotations to be untrusted unless they come from trusted servers”2026-07-28/server/tools#toolyes — mcpx forwards upstream annotations; downstream must treat them as upstream’s, mcpx must not launder trustyes — mcpx must not act on upstream annotationsnew in 2025-03-26 (annotations added)
tools-annotations-hints-no-decisions2025-03-26 – 2026-07-28clientshould (lowercase, schema)tools“Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers.”2026-07-28/schema#toolannotationsn/ayesschema-only, lowercase
tools-inputschema-valid-not-null2025-11-25 – 2026-07-28serverMUSTtools / json-schema“MUST be a valid JSON Schema object (not null)”2026-07-28/server/tools#toolyes — generated ops must emit object schemas, forwarded ones should be checkedyes — tolerate/flag invalid upstreamexplicit from 2025-11-25; schema required type:"object" since 2024-11-05
tools-inputschema-type-object-root2024-11-05 – 2026-07-28serverREQUIRED (schema)tools / json-schema“Tool arguments are always JSON objects, so type: \"object\" is required at the root.”2026-07-28/schema#toolyesyes2026 comment adds “any JSON Schema 2020-12 keyword may appear”; earlier schemas only allowed properties/required shape
tools-inputschema-no-params-recommended2025-11-25 – 2026-07-28serverRECOMMENDEDtools“{ \"type\": \"object\", \"additionalProperties\": false } - Recommended”2026-07-28/server/tools#toolyesno
tools-schema-default-2020-122025-11-25 – 2026-07-28both(definition)json-schema“Defaults to 2020-12 if no $schema field is present”2026-07-28/server/tools#toolyesyes2025-11-25 is when $schema/dialects arrive
tools-x-mcp-header-may2026-07-28serverMAYtools“Properties MAY include an x-mcp-header annotation to expose parameter values as HTTP headers”2026-07-28/server/tools#toolnot-implemented-by-choice — mcpx-generated tools need none; forwarded upstream schemas may carry ityes — must handle when upstream declares itnew
tools-x-mcp-header-not-empty2026-07-28serverMUST NOTtools“MUST NOT be empty”2026-07-28/server/tools#x-mcp-headeryes if forwarding upstream schemas verbatimyes (validation side, see reject row)new
tools-x-mcp-header-tchar2026-07-28serverMUSTtools“MUST match HTTP field-name token syntax (1*tchar…)”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-no-ctl2026-07-28serverMUST NOTtools“MUST NOT contain control characters, including carriage return… or line feed”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-unique-ci2026-07-28serverMUSTtools“MUST be case-insensitively unique among all x-mcp-header values in the inputSchema”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-primitive-only2026-07-28serverMUSTtools“MUST only be applied to parameters with primitive types (integer, string, boolean). Parameters with type number are not permitted.”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-int-safe-range2026-07-28serverMUSTtools“Integer values MUST be within the safe range for integers represented using IEEE754 double-precision”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-statically-reachable2026-07-28serverMUSTtools“MUST only be applied to properties that are statically reachable from the schema root”2026-07-28/server/tools#x-mcp-headersamesamenew
tools-x-mcp-header-reject-excludes2026-07-28clientMUSTtools“Rejection means the client MUST exclude the invalid tool from the result of tools/list”2026-07-28/server/tools#x-mcp-headernoyes — excluded from what mcpx re-exposes toonew
tools-x-mcp-header-reject-log2026-07-28clientSHOULDtools“Clients SHOULD log a warning when rejecting a tool definition, including the tool name and the reason”2026-07-28/server/tools#x-mcp-headernoyesnew
tools-x-mcp-header-stdio-may-ignore2026-07-28clientMAYtools“Clients using other transports (e.g., stdio) MAY ignore x-mcp-header annotations entirely”2026-07-28/server/tools#x-mcp-headernoyes — stdio upstreamsnew
tools-x-mcp-header-no-secrets2026-07-28serverSHOULD NOTtools“Server developers SHOULD NOT mark sensitive parameters (passwords, API keys, tokens, PII) with x-mcp-header”2026-07-28/server/tools#x-mcp-headeryes (own tools)nonew
tools-name-length2025-11-25 – 2026-07-28serverSHOULDtools“Tool names SHOULD be between 1 and 128 characters in length (inclusive).”2026-07-28/server/tools#tool-namesyes — namespaced names (server+tool) can exceed 128; checkno (tolerate)new 2025-11-25 (SEP-986)
tools-name-case-sensitive2025-11-25 – 2026-07-28bothSHOULDtools“Tool names SHOULD be considered case-sensitive.”2026-07-28/server/tools#tool-namesyesyesnew 2025-11-25
tools-name-charset2025-11-25 – 2026-07-28serverSHOULDtools“The following SHOULD be the only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.)”2026-07-28/server/tools#tool-namesyes — namespacing separator must be one of _ - .yes — upstream names outside charset must be tolerated/mappednew 2025-11-25
tools-name-no-special2025-11-25 – 2026-07-28serverSHOULD NOTtools“Tool names SHOULD NOT contain spaces, commas, or other special characters.”2026-07-28/server/tools#tool-namesyesnonew
tools-name-unique-in-server2025-11-25 – 2026-07-28serverSHOULDtools“Tool names SHOULD be unique within a server.”2026-07-28/server/tools#tool-namesyes — mcpx is one server to its client; aggregated names must be uniquenonew
tools-aggregator-disambiguate2026-07-28clientSHOULDtools“Clients or proxies that aggregate tools from multiple servers… SHOULD implement a disambiguation strategy such as prefixing tool names with a server identifier.”2026-07-28/server/tools#tool-namesyes — addressed at mcpx by name (“proxies”)yesnew; applies in spirit to all revs
tools-aggregator-not-by-servername2026-07-28clientSHOULD NOTtools“The server name (from serverInfo) is not guaranteed to be unique across servers and SHOULD NOT be relied upon for disambiguation.”2026-07-28/server/tools#tool-namesyes — prefix with mcpx’s config key, not upstream serverInfo.nameyesnew
tools-call-input-required-may2026-07-28serverMAYtools / mrtr“Servers MAY respond to tools/call with an InputRequiredResult”2026-07-28/server/tools#input-required-tool-resultsyes — viaAsk (§3.3)yes — must resolve and retrynew
tools-call-inputrequests-declared-only2026-07-28serverMUST NOTtools / mrtr“Servers MUST NOT send an inputRequests that the client has not declared support for in its capabilities.” (mrtr.mdx)2026-07-28/basic/patterns/mrtryes — §3.1 table enforces itnonew; cross-page, affects tools/call, prompts/get, resources/read
tools-result-iserror-for-tool-errors2024-11-05 – 2026-07-28serverSHOULDtools“Any errors that originate from the tool SHOULD be reported inside the result object, with isError set to true, not as an MCP protocol-level error response.”2026-07-28/schema#calltoolresultyes — server.go tools/call returns isErroryes — forwards upstream isError unchangedschema comment, unchanged
tools-result-lookup-errors-protocol2024-11-05 – 2026-07-28servershould (lowercase)tools“any errors in finding the tool… should be reported as an MCP error response”2026-07-28/schema#calltoolresultyes — an unknown tool is -32602 (internal/mcpserver/server.go no tool named, #284); tested, see conformance-matrix.mdyes
tools-error-unknown-tool-protocol2024-11-05 – 2026-07-28server(classification)tools“Protocol Errors … Unknown tool” ; example code -326022026-07-28/server/tools#error-handlingyes (as above); tested, see conformance-matrix.mdyes
tools-error-invalid-args-protocol2024-11-05 – 2025-06-18server(classification)tools“Protocol Errors: Standard JSON-RPC errors for issues like: … Invalid arguments”2025-06-18/server/tools#error-handlingyes — mcpx returns isError for dispatch failures, which the later rule prefers; acceptable to legacy clientsyesCHANGED in 2025-11-25
tools-error-input-validation-execution2025-11-25 – 2026-07-28server(classification)tools“Tool Execution Errors … Input validation errors (e.g., date in wrong format, value out of range)”; only “Malformed requests (requests that fail to satisfy CallToolRequest schema)” stay protocol errors2026-07-28/server/tools#error-handlingyesyesCHANGED from “Invalid arguments” (SEP-1303)
tools-client-exec-errors-to-llm2025-11-25 – 2026-07-28clientSHOULDtools“Clients SHOULD provide tool execution errors to language models to enable self-correction.”2026-07-28/server/tools#error-handlingnoyes — must pass isError results through, not convert to protocol errorsnew 2025-11-25
tools-client-protocol-errors-to-llm-may2025-11-25 – 2026-07-28clientMAYtools“Clients MAY provide protocol errors to language models”2026-07-28/server/tools#error-handlingnoyesnew
tools-embedded-resource-may2024-11-05 – 2026-07-28serverMAYtools“Resources MAY be embedded”2026-07-28/server/tools#embedded-resourcesyes — resource_link→resource downgrade (§2.3)yes
tools-embedded-implies-resources-cap2025-06-18 – 2026-07-28serverSHOULDtools“Servers that use embedded resources SHOULD implement the resources capability”2026-07-28/server/tools#embedded-resourcesyes — declared; but downgraded links become embedded resources whose URIs are upstream’s, not readable via mcpx unless proxiednonew 2025-06-18
tools-resource-link-may2025-06-18 – 2026-07-28serverMAYtools“A tool MAY return links to Resources”2026-07-28/server/tools#resource-linksyes (≥2025-06-18); downgraded for 2025-03-26yesnew 2025-06-18
tools-resource-link-may-not-listed2025-06-18 – 2026-07-28both(note)tools“Resource links returned by tools are not guaranteed to appear in the results of a resources/list request.”2026-07-28/server/tools#resource-linksyesyes
tools-structured-also-text2025-06-18 – 2026-07-28serverSHOULDtools“For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.”2026-07-28/server/tools#structured-contentyes — and for 2025-03-26 structuredContent removed + rendered (§2.3)nonew 2025-06-18
tools-structured-any-json2026-07-28server(type)tools“This can be any JSON value (object, array, string, number, boolean, or null)”2026-07-28/server/tools#structured-contentyes — must NOT send non-object structuredContent to 2025-06-18/2025-11-25 clientsyes — accept any JSON from modern upstreamCHANGED: 2025-06-18/11-25 schema { [key: string]: unknown } (object only)
tools-structured-object-only2025-06-18 – 2025-11-25server(type, schema)tools“structuredContent?: { [key: string]: unknown };”2025-11-25/schema#calltoolresultyes — downgrade must wrap/strip non-object valuesyesCHANGED in 2026
tools-output-schema-conform2025-06-18 – 2026-07-28serverMUSTtools“Servers MUST provide structured results that conform to this schema.”2026-07-28/server/tools#output-schemayes — for forwarded tools mcpx passes upstream’s; if it rewrites structuredContent it must still conformno
tools-output-schema-client-validate2025-06-18 – 2026-07-28clientSHOULDtools“Clients SHOULD validate structured results against this schema.”2026-07-28/server/tools#output-schemanoyes — not-implemented-by-choice likely; pass-through
tools-output-schema-object-root2025-06-18 – 2025-11-25server(type, schema)tools“Currently restricted to type: "object" at the root level.”2025-11-25/schema#toolyes — must not forward non-object outputSchema to legacy clientsyesCHANGED in 2026: “This can be any valid JSON Schema 2020-12.”
tools-display-name-precedence2025-06-18 – 2026-07-28client(rule)tools“Display name precedence order is: title, annotations.title, then name.”2026-07-28/schema#toolyesyes
tools-server-sec-validate-inputs2024-11-05 – 2026-07-28serverMUSTtools“Servers MUST: Validate all tool inputs; Implement proper access controls; Rate limit tool invocations; Sanitize tool outputs”2026-07-28/server/tools#security-considerationsyes — rate limiting / access control: not-implemented-by-choice (loopback, no auth, protocol.md §5)no
tools-client-sec2024-11-05 – 2025-11-25clientSHOULDtools“Clients SHOULD: Prompt for user confirmation…; Show tool inputs…; Validate tool results before passing to LLM; Implement timeouts for tool calls; Log tool usage for audit purposes”2025-11-25/server/tools#security-considerationsnoyes — timeouts + logging (logstore) yes; confirmation not-implemented-by-choice
tools-client-sec-ref-resolution2026-07-28clientSHOULDtools / json-schema“Follow the $ref resolution requirements when validating tool inputs and outputs”2026-07-28/server/tools#security-considerationsnoyes if validating; otherwise n/aadded 2026
resources-declare-capability2024-11-05 – 2026-07-28serverMUSTresources“Servers that support resources MUST declare the resources capability”2026-07-28/server/resources#capabilitiesyesno
resources-subscribe-cap-meaning-legacy2024-11-05 – 2025-11-25server(definition)resources“subscribe: whether the client can subscribe to be notified of changes to individual resources.”2025-11-25/server/resources#capabilitiesyes — declaredyes — call resources/subscribe only if declaredCHANGED meaning 2026
resources-subscribe-cap-meaning-20262026-07-28server(definition)resources“subscribe : whether the server supports resource-specific update notifications for resources requested through subscriptions/listen using the resourceSubscriptions filter.”2026-07-28/server/resources#capabilitiesyesyes — only send resourceSubscriptions if declaredCHANGED
resources-cap-features-independent2024-11-05 – 2026-07-28serverMAYresources“Servers may advertise either feature independently, together or neither.” / “Servers that support neither… may omit it”2026-07-28/server/resources#capabilitiesyesyes — resources: {} is valid
resources-list-must-respond-when-declared2026-07-28serverMUSTresources“Servers that declare the resources capability MUST respond to resources/list requests with the set of resources currently available to the requesting client.”2026-07-28/server/resources#capabilitiesyes — returns [] when no backendnonew
resources-list-not-per-connection2026-07-28serverMUST NOTresources“MUST NOT vary per-connection or as a side effect of other requests on the connection”2026-07-28/server/resources#capabilitiesyesnoCHANGED (new constraint)
resources-list-may-vary-by-auth2026-07-28serverMAYresources“The set MAY vary by the authorization presented on the request”2026-07-28/server/resources#capabilitiesnot-implemented-by-choice — no authnonew
resources-list-changed-notify2024-11-05 – 2026-07-28serverSHOULDresources“servers that declared the listChanged capability SHOULD send a notification”2026-07-28/server/resources#list-changed-notificationyesyestext unchanged in 2026 but see Conflicts C3
resources-read-multiple-may2026-07-28serverMAYresources“Servers MAY return multiple resource contents in response to a single resources/read request.”2026-07-28/server/resources#reading-resourcesyes (single today)yes — must accept arrays of >1explicit only in 2026; schema always allowed an array
resources-read-input-required-may2026-07-28serverMAYresources / mrtr“Servers MAY also respond to resources/read with an InputRequiredResult”2026-07-28/server/resources#reading-resourcesyes — viaAskyesnew
resources-read-https-direct-may2026-07-28clientmay (lowercase)resources“if the scheme of uri is https://, clients may fetch the resource directly from the web”2026-07-28/server/resources#reading-resourcesnonot-implemented-by-choicenew in 2026 prose
resources-subscribe-rpc2024-11-05 – 2025-11-25client(optional)resources / subscriptions“The protocol supports optional subscriptions to resource changes. Clients can subscribe to specific resources” (resources/subscribe)2025-11-25/server/resources#subscriptionsyes — accepted in any era (protocol.md §2.1)yes — legacy upstreamsREMOVED in 2026
resources-unsubscribe-follows-subscribe2024-11-05 – 2025-11-25clientshould (lowercase, schema)resources / subscriptions“This should follow a previous resources/subscribe request.”2025-11-25/schema#unsubscriberequestyesyesremoved 2026
resources-subscribe-via-listen2026-07-28client(mechanism)resources / subscriptions“Clients subscribe to change notifications for specific resources by sending a subscriptions/listen request with the resource URIs listed in notifications.resourceSubscriptions.”2026-07-28/server/resources#subscriptionsyes — any erayes — modern upstreamsCHANGED: replaces resources/subscribe
resources-updated-may-be-subresource2024-11-05 – 2026-07-28client(note, schema)resources“This might be a sub-resource of the one that the client actually subscribed to.”2026-07-28/schema#resourceupdatednotificationyes — fan-out must match by prefix/subresource, not equalityyes
resources-https-only-if-client-fetchable2024-11-05 – 2026-07-28serverSHOULDresources“Servers SHOULD use this scheme only when the client is able to fetch and load the resource directly from the web on its own”2026-07-28/server/resources#httpsyesno
resources-https-else-other-scheme2024-11-05 – 2026-07-28serverSHOULDresources“servers SHOULD prefer to use another URI scheme, or define a custom one”2026-07-28/server/resources#httpsyesno
resources-file-xdg-mime-may2024-11-05 – 2026-07-28serverMAYresources“MCP servers MAY identify file:// resources with an XDG MIME type, like inode/directory”2026-07-28/server/resources#fileyesyes — accept
resources-custom-scheme-rfc39862025-06-18 – 2026-07-28serverMUSTresources“Custom URI schemes MUST be in accordance with RFC3986”2026-07-28/server/resources#custom-uri-schemesyes — any mcpx-minted URIs (namespacing upstream URIs) must stay RFC3986nonew 2025-06-18
resources-not-found-320022024-11-05 – 2025-11-25serverSHOULDresources / errors“Servers SHOULD return standard JSON-RPC errors for common failure cases: Resource not found: -32002”2025-11-25/server/resources#error-handlingyes — -32002 with data.uri through 2025-11-25, -32602 in 2026-07-28 (codeResourceNotFoundLegacy); tested, see conformance-matrix.mdyes — accept -32002CHANGED 2026
resources-internal-326032024-11-05 – 2026-07-28serverSHOULDresources / errors“Internal errors: -32603” / “Servers SHOULD return -32603 for internal errors.”2026-07-28/server/resources#error-handlingyes — backend read failures are -32603 (codeInternal); tested, see conformance-matrix.mdyes
resources-not-found-326022026-07-28serverMUSTresources / errors“If the requested resource does not exist, servers MUST return a JSON-RPC error with code -32602 (Invalid Params).”2026-07-28/server/resources#error-handlingyesyesCHANGED from SHOULD -32002
resources-client-accept-320022026-07-28clientSHOULDresources / errors“clients SHOULD also accept -32002 as a resource not found error”2026-07-28/server/resources#error-handlingnoyes — legacy upstreamsnew
resources-no-empty-contents-for-missing2026-07-28serverMUST NOTresources“Servers MUST NOT return an empty contents array for a non-existent resource.”2026-07-28/server/resources#error-handlingyesyes — treat empty as “exists, no content”new (applies sensibly to all eras)
resources-sec-validate-uris2024-11-05 – 2026-07-28serverMUSTresources“Servers MUST validate all resource URIs”2026-07-28/server/resources#security-considerationsyesno
resources-sec-access-control2024-11-05 – 2026-07-28serverSHOULDresources“Access controls SHOULD be implemented for sensitive resources”2026-07-28/server/resources#security-considerationsnot-implemented-by-choice — no authno
resources-sec-binary-encoded2024-11-05 – 2026-07-28serverMUSTresources“Binary data MUST be properly encoded”2026-07-28/server/resources#security-considerationsyes — mcpx read path returns text only; blobs from upstream must be passed as base64 blobyes
resources-sec-permissions2024-11-05 – 2026-07-28serverSHOULDresources“Resource permissions SHOULD be checked before operations”2026-07-28/server/resources#security-considerationsnot-implemented-by-choiceno
resources-sec-file-traversal2026-07-28serverMUSTresources“Servers MUST sanitize file paths to prevent directory traversal attacks when serving file:// resources”2026-07-28/server/resources#security-considerationsyes only if mcpx serves file:// itself; forwarding delegates to upstreamnonew
resources-annotations-defs2025-06-18 – 2026-07-28both(definition)resources“priority: A number from 0.0 to 1.0”; “audience… Valid values are \"user\" and \"assistant\"”; “lastModified: An ISO 8601 formatted timestamp”2026-07-28/server/resources#annotationsyes — pass throughyeslastModified new in 2025-06-18; audience/priority in schema since 2024-11-05
prompts-declare-capability2024-11-05 – 2025-11-25serverMUSTprompts“Servers that support prompts MUST declare the prompts capability during initialization”2025-11-25/server/prompts#capabilitiesyesnoCHANGED location
prompts-declare-capability-discover2026-07-28serverMUSTprompts“Servers that support prompts MUST declare the prompts capability in their DiscoverResult”2026-07-28/server/prompts#capabilitiesyes — but mcpx’s DiscoverResult field names are wrong (see discover rows)noCHANGED
prompts-list-must-respond-when-declared2026-07-28serverMUSTprompts“Servers that declare the prompts capability MUST respond to prompts/list requests with the set of prompts currently available to the requesting client.”2026-07-28/server/prompts#capabilitiesyesnonew
prompts-list-not-per-connection2026-07-28serverMUST NOTprompts“MUST NOT vary per-connection or as a side effect of other requests on the connection”2026-07-28/server/prompts#capabilitiesyesnonew
prompts-list-may-vary-by-auth2026-07-28serverMAYprompts“The set MAY vary by the authorization presented on the request”2026-07-28/server/prompts#capabilitiesnot-implemented-by-choicenonew
prompts-list-changed-notify2024-11-05 – 2025-11-25serverSHOULDprompts“servers that declared the listChanged capability SHOULD send a notification”2025-11-25/server/prompts#list-changed-notificationyesyesCHANGED 2026
prompts-list-changed-notify-to-listeners2026-07-28serverSHOULDprompts“SHOULD send a notification to clients that have opened a subscriptions/listen stream with promptsListChanged: true”2026-07-28/server/prompts#list-changed-notificationyesyesCHANGED
prompts-get-input-required-may2026-07-28serverMAYprompts / mrtr“Servers MAY also respond to prompts/get with an InputRequiredResult”2026-07-28/server/prompts#getting-a-promptyes — viaAskyesnew
prompts-image-base64-mime2024-11-05 – 2026-07-28serverMUSTprompts“The image data MUST be base64-encoded and include a valid MIME type.”2026-07-28/server/prompts#image-contentyes (forwarded)yes
prompts-audio-base64-mime2025-03-26 – 2026-07-28serverMUST (not bolded)prompts“The audio data MUST be base64-encoded and include a valid MIME type.”2026-07-28/server/prompts#audio-contentyesyesnew 2025-03-26
prompts-resource-link-may2026-07-28serverMAYprompts“Prompt messages MAY include links to Resources”2026-07-28/server/prompts#resource-linksyesyesprose new in 2026; see Conflicts C9
prompts-embedded-must-include2024-11-05 – 2026-07-28serverMUSTprompts“Resources can contain either text or binary (blob) data and MUST include: A valid resource URI; The appropriate MIME type; Either text content or base64-encoded blob data”2026-07-28/server/prompts#embedded-resourcesyesyessee Conflicts C8 (schema: mimeType optional)
prompts-errors-32602-326032024-11-05 – 2026-07-28serverSHOULDprompts / errors“Invalid prompt name: -32602… Missing required arguments: -32602… Internal errors: -32603”2026-07-28/server/prompts#error-handlingyes — unknown prompt -32602, backend failure -32603; tested, see conformance-matrix.mdyes
prompts-validate-args2024-11-05 – 2026-07-28serverSHOULDprompts“Servers SHOULD validate prompt arguments before processing”2026-07-28/server/prompts#implementation-considerationsyesno
prompts-client-paginate2024-11-05 – 2026-07-28clientSHOULDprompts / pagination“Clients SHOULD handle pagination for large prompt lists”2026-07-28/server/prompts#implementation-considerationsnoyes — follow nextCursor upstream
prompts-respect-capability-negotiation2024-11-05 – 2026-07-28bothSHOULDprompts“Both parties SHOULD respect capability negotiation”2026-07-28/server/prompts#implementation-considerationsyesyes
prompts-validate-io2024-11-05 – 2026-07-28bothMUSTprompts“Implementations MUST carefully validate all prompt inputs and outputs to prevent injection attacks or unauthorized access to resources.”2026-07-28/server/prompts#securityyesyes
meta-request-required-fields2026-07-28clientMUSTmeta“Every request MUST include the required _meta fields” (tools/resources/prompts page notes)2026-07-28/server/toolsyes (checks inbound)yes — every modern upstream requestnew
cache-ttl-scope-required2026-07-28serverREQUIRED (schema)cachingttlMs: number; cacheScope: "public" | "private"; (non-optional on CacheableResult)2026-07-28/schema#discoverresultyes — ttlMs/cacheScope on discover, the three lists, templates/list and resources/read (internal/mcpserver/server.go cacheable); tested, see conformance-matrix.mdyes — may use for upstream cachenew
discover-client-may-call2026-07-28clientMAYdiscover“Clients MAY call it but are not required to” (schema); “Calling server/discover is optional for clients”2026-07-28/server/discover#when-to-callnoyesnew
discover-supportedversions2026-07-28serverREQUIRED (schema)discover“supportedVersions: Protocol versions the server supports.”2026-07-28/server/discover#discoverresultyes — supportedVersions (#224); tested, see conformance-matrix.mdyes — read supportedVersionsnew
discover-client-choose-from-list2026-07-28clientshould (lowercase)discover“The client should choose one of these for subsequent requests.”2026-07-28/server/discover#discoverresultnoyesnew
discover-capabilities2026-07-28serverREQUIRED (schema)discover“capabilities: Capabilities the server supports”2026-07-28/server/discover#discoverresultyesyesreplaces InitializeResult.capabilities
discover-serverinfo-in-meta2026-07-28serverSHOULDdiscover“_meta['io.modelcontextprotocol/serverInfo']: Name and version of the server software. Servers SHOULD include this field.”2026-07-28/server/discover#discoverresultyes — _meta[io.modelcontextprotocol/serverInfo], no top-level serverInfo; tested, see conformance-matrix.mdyesCHANGED location vs InitializeResult.serverInfo (top-level, required)
discover-serverinfo-not-for-behavior2026-07-28clientSHOULD NOTdiscover“Clients SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for security decisions.”2026-07-28/server/discover#discoverresultnoyesnew
discover-instructions-optional2026-07-28serverMAYdiscover“instructions: Optional natural-language guidance for LLMs”; schema: “should not duplicate information already in tool descriptions”2026-07-28/server/discover#discoverresultyesyeslegacy counterpart in InitializeResult
discover-cacheable2026-07-28serverREQUIRED (schema)discover / caching“This operation supports caching.” ; DiscoverResult extends CacheableResult2026-07-28/server/discover#responseyes — ttlMs/cacheScope; tested, see conformance-matrix.mdyesnew
discover-resulttype2026-07-28serverMUSTdiscover / metaexample "resultType": "complete"; Result: “Servers implementing this protocol version MUST include this field.”2026-07-28/server/discover#responseyes if discover goes through the resultType stamping path — verifyyesnew
discover-request-meta-required2026-07-28clientMUSTdiscover / meta“The request carries no body parameters beyond the standard _meta” (+ required _meta fields)2026-07-28/server/discover#requestyesyesnew

Roots, sampling, elicitation

idrevssidelevelareaquoteurlmcpx-as-servermcpx-as-clientchanges
roots-client-declare-capability-at-initialize2024-11-05 – 2025-11-25clientMUSTroots“Clients that support roots MUST declare the roots capability during initialization”https://modelcontextprotocol.io/specification/2025-11-25/client/roots#capabilitiesn/a (mcpx never asks a downstream client for roots)yes — declared in initialize (roots:{listChanged:false})replaced in 2026-07-28 by per-request _meta
roots-client-declare-capability-per-request-meta2026-07-28clientMUSTroots“Clients that support roots MUST declare the roots capability in _meta.io.modelcontextprotocol/clientCapabilities on each request”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#capabilitiesn/ayes — withMeta stamps caps on every modern request; note mcpx sends roots:{listChanged:false} where the 2026 schema type is roots?: {} (extra key; harmless under open objects)new; listChanged gone
roots-client-listchanged-must-notify2024-11-05 – 2025-11-25clientMUSTroots“When roots change, clients that support listChanged MUST send a notification”https://modelcontextprotocol.io/specification/2025-11-25/client/roots#root-list-changesn/ano — mcpx declares listChanged:false, so the obligation never attachesremoved in 2026-07-28 (notifications/roots/list_changed removed, changelog item 5)
roots-server-list-via-input-required2026-07-28serverMUST (via MRTR note)roots“servers send an InputRequiredResult containing a roots/list request”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#listing-rootsnot-implemented-by-choice — mcpx answers roots itself and never asks downstreamyes — resolveInput answers roots/list inside inputRequests via the same answer() as legacylegacy: server sends roots/list as a JSON-RPC request
roots-uri-must-be-file-scheme2024-11-05 – 2026-07-28clientMUSTroots“This MUST be a file:// URI in the current specification.” (schema: “This must start with file:// for now.”)https://modelcontextprotocol.io/specification/2026-07-28/client/roots#rootn/ayes — roots come from config; whether config rejects non-file:// was not verified
roots-client-only-expose-permitted-roots2024-11-05 – 2026-07-28clientMUSTroots“Clients MUST: - Only expose roots with appropriate permissions”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/ayes — operator-configured roots are the permission decision
roots-client-validate-uris-path-traversal2024-11-05 – 2026-07-28clientMUSTroots“Validate all root URIs to prevent path traversal”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/ayes — not verified that configured roots are validated
roots-client-implement-access-controls2024-11-05 – 2026-07-28clientMUSTroots“Implement proper access controls”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/ayescontradicts 2026 intro “informational guidance rather than an access-control mechanism” (see Conflicts)
roots-client-monitor-accessibility2024-11-05 – 2026-07-28clientMUSTroots“Monitor root accessibility”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/ayes — not done (static config)
roots-server-handle-unavailable-roots2024-11-05 – 2026-07-28serverSHOULDroots“Servers SHOULD: - Handle cases where roots become unavailable”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/a (mcpx does not consume roots)n/a (peer’s duty)
roots-server-respect-boundaries2024-11-05 – 2026-07-28serverSHOULDroots“Respect root boundaries during operations”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/an/a (peer’s duty)repeated under Implementation Guidelines
roots-server-validate-paths-against-roots2024-11-05 – 2026-07-28serverSHOULDroots“Validate all paths against provided roots”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerationsn/an/a (peer’s duty)
roots-client-prompt-consent-before-exposing2024-11-05 – 2026-07-28clientSHOULDroots“Prompt users for consent before exposing roots to servers”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/ayes — config is the consent; no prompt
roots-client-provide-root-management-ui2024-11-05 – 2026-07-28clientSHOULDroots“Provide clear user interfaces for root management”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/ayes — config file only
roots-client-validate-accessibility-before-exposing2024-11-05 – 2026-07-28clientSHOULDroots“Validate root accessibility before exposing”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/ayes
roots-client-monitor-root-changes2024-11-05 – 2026-07-28clientSHOULDroots“Monitor for root changes”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/ayes2026 keeps this although it removed the change notification
roots-server-check-capability-before-use2024-11-05 – 2026-07-28serverSHOULDroots“Check for roots capability before usage”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/an/a (peer’s duty)hardened to MUST NOT in 2026 by mrtr (row mrtr-server-no-undeclared-input-requests)
roots-server-handle-list-changes2024-11-05 – 2025-11-25serverSHOULDroots“Handle root list changes gracefully”https://modelcontextprotocol.io/specification/2025-11-25/client/roots#implementation-guidelinesn/an/aremoved in 2026-07-28
roots-server-cache-root-information2024-11-05 – 2026-07-28serverSHOULDroots“Cache root information appropriately”https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelinesn/an/ain 2026 there is no session to cache against (see Implied)
roots-client-return-jsonrpc-errors2024-11-05 – 2025-11-25clientSHOULDroots“Clients SHOULD return standard JSON-RPC errors for common failure cases: - Client does not support roots: -32601 (Method not found) - Internal errors: -32603”https://modelcontextprotocol.io/specification/2025-11-25/client/roots#error-handlingn/ayes — unknown methods get -32601; roots always answered2026: “the client does not need to replay the initial call with an error message” — no error path
roots-deprecated-new-impls-should-not-adopt2026-07-28bothSHOULD NOTroots“New implementations SHOULD NOT adopt it”https://modelcontextprotocol.io/specification/2026-07-28/client/rootsnot-implemented-by-choice (never asks)yes — mcpx is an existing implementation; keeps answeringnew (SEP-2577)
roots-deprecated-existing-should-migrate2026-07-28bothSHOULDroots“existing implementations SHOULD migrate to passing directories or files via tool parameters, resource URIs, or server configuration”https://modelcontextprotocol.io/specification/2026-07-28/client/rootsn/ayes — configured roots already are server configuration; migration is upstream’snew
sampling-human-in-loop-can-deny2024-11-05 – 2026-07-28clientSHOULDsampling“there SHOULD always be a human in the loop with the ability to deny sampling requests”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#user-interaction-modeln/a (downstream’s duty; mcpx forwards)yes — delegated to broker/downstream client; mcpx itself has no model
sampling-app-review-ui2024-11-05 – 2026-07-28clientSHOULDsampling“Provide UI that makes it easy and intuitive to review sampling requests”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#user-interaction-modeln/ayes — delegated
sampling-app-view-edit-prompts2024-11-05 – 2026-07-28clientSHOULDsampling“Allow users to view and edit prompts before sending”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#user-interaction-modeln/ayes — delegated
sampling-app-review-responses2024-11-05 – 2026-07-28clientSHOULDsampling“Present generated responses for review before delivery”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#user-interaction-modeln/ayes — delegated
sampling-client-declare-capability-at-initialize2024-11-05 – 2025-11-25clientMUSTsampling“Clients that support sampling MUST declare the sampling capability during initialization”https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#capabilitiesn/ayes — declared only when a handler is installedreplaced in 2026-07-28
sampling-client-declare-capability-per-request-meta2026-07-28clientMUSTsampling“Clients that support sampling MUST declare the sampling capability in _meta.io.modelcontextprotocol/clientCapabilities on each request”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#capabilitiesn/ayesnew
sampling-client-declare-tools-capability2025-11-25 – 2026-07-28clientMUSTsampling“Clients MUST declare support for tool use via the sampling.tools capability to receive tool-enabled sampling requests.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tools-in-samplingn/ayes — mcpx never declares sampling.toolsnew in 2025-11-25
sampling-server-no-tools-without-capability2025-11-25 – 2026-07-28serverMUST NOTsampling“Servers MUST NOT send tool-enabled sampling requests to Clients that have not declared support for tool use via the sampling.tools capability.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tools-in-samplingyes — Question.Sendable refuses a tools-carrying sampling question unless the peer declared sampling.tools (Peer.CanSampleTools)n/a (peer’s duty)new in 2025-11-25
sampling-client-error-on-tools-without-capability2025-11-25 – 2026-07-28clientMUSTsampling“The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared.” (schema, on tools and on toolChoice)https://modelcontextprotocol.io/specification/2026-07-28/schema#createmessagerequestparamsn/ayes — checked in mcpclient answer(): a request carrying tools or toolChoice is refused -32602 naming sampling.tools before the handler runs, in both eras (in 2026 the refusal surfaces as the call’s error, there being no error slot in inputResponses)new in 2025-11-25
sampling-server-includecontext-only-with-context-capability2025-11-25 – 2026-07-28serverSHOULD NOTsampling“Servers SHOULD avoid using these values … and SHOULD NOT use them unless the client declares sampling.context capability.” (schema: “If not declared, servers SHOULD only use includeContext: \"none\" (or omit it).”)https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#capabilitiesyes — Question.paramsFor drops includeContext unless the peer declared sampling.context (Peer.CanSampleContext)n/a (peer’s duty); mcpx declares no sampling.context“soft-deprecated” in 2025-11-25 → “deprecated” (SEP-2596) in 2026-07-28; obligation text unchanged
sampling-server-avoid-includecontext-values2025-11-25 – 2026-07-28serverSHOULDsampling“Servers SHOULD avoid using these values (e.g. can just omit includeContext since it defaults to \"none\")”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#capabilitiesyes (as above)n/a
sampling-deprecated-new-impls-should-not-adopt2026-07-28bothSHOULD NOTsampling“New implementations SHOULD NOT adopt it; existing implementations SHOULD migrate to integrating directly with LLM provider APIs.”https://modelcontextprotocol.io/specification/2026-07-28/client/samplingyes — mcpx forwards; pass-through, not adoptionyesnew (SEP-2577)
sampling-tool-result-message-only-tool-results2025-11-25 – 2026-07-28bothMUSTsampling“When a user message contains tool results (type: "tool_result"), it MUST contain ONLY tool results.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tool-result-messagesyes — pass-through; mcpx does not rewrite messagesyes — pass-throughnew
sampling-tool-use-result-balance2025-11-25 – 2026-07-28bothMUSTsampling“every assistant message containing ToolUseContent blocks MUST be followed by a user message that consists entirely of ToolResultContent blocks, with each tool use … matched by a corresponding tool result”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tool-use-and-result-balanceyes — pass-throughyes — pass-throughnew
sampling-server-answer-every-tooluse2025-11-25 – 2026-07-28serverMUSTsampling“Servers MUST ensure that when replying to a stopReason: \"toolUse\", each ToolUseContent item is responded to with a ToolResultContent item with a matching toolUseId”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsyes — upstream’s content, forwardedn/anew
sampling-toolresult-id-must-match-tooluse2025-11-25 – 2026-07-28bothMUSTsampling“This MUST match the ID from a previous ToolUseContent.”https://modelcontextprotocol.io/specification/2026-07-28/schema#toolresultcontentyes — ids must not be rewritten in transityesnew
sampling-toolchoice-required-model-must-use-tool2025-11-25 – 2026-07-28clientMUSTsampling“{mode: \"required\"}: Model MUST use at least one tool before completing”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tool-choice-modesn/an/a — mcpx never declares toolsnew
sampling-toolchoice-none-model-must-not-use-tools2025-11-25 – 2026-07-28clientMUST NOTsampling“{mode: \"none\"}: Model MUST NOT use any tools”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tool-choice-modesn/an/anew
sampling-parallel-tool-toggle-extension-only2025-11-25 – 2026-07-28clientMAYsampling“Implementations wrapping providers that support disabling parallel tool use MAY expose this as an extension, but it is not part of the core MCP specification.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#parallel-tool-usen/an/a
sampling-tooluse-meta-preserve2025-11-25 – 2026-07-28client (text says “Clients”; see Conflicts)SHOULDsampling“Clients SHOULD preserve this field when including tool uses in subsequent sampling requests to enable caching optimizations.” (same on ToolResultContent._meta)https://modelcontextprotocol.io/specification/2026-07-28/schema#toolusecontentyes — forward _meta on content blocks untouchedyesnew
sampling-toolresult-structured-conform-outputschema2025-11-25 – 2026-07-28bothSHOULDsampling“If the tool defined an outputSchema, this SHOULD conform to that schema.”https://modelcontextprotocol.io/specification/2026-07-28/schema#toolresultcontentyes — pass-throughyes — pass-through
sampling-message-must-have-role-and-content2026-07-28bothMUSTsampling“Sampling messages MUST contain a role field of \"user\" or \"assistant\"; and a content field representing the message data.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#messagesyesyes — daemon fills a missing role/model in results (hooks.go:223-229)prose new in 2026; schema had the fields required since 2024-11-05
sampling-messages-not-retained-between-requests2026-07-28clientSHOULD NOTsampling“The list of messages in a sampling request SHOULD NOT be retained between separate requests.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#messagesn/ayes — broker persists questions in SQLite (docs/elicitation.md §3); a sampling question’s messages are retained (conflict with this SHOULD NOT; unverified whether params are stored)new in 2026
sampling-client-must-respect-maxtokens2026-07-28clientMUSTsampling“The client MUST respect the maxTokens parameter.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#sampling-parametersyes — forward maxTokens unchangedyes — delegated to whoever samplesnew in 2026 prose
sampling-client-may-sample-fewer-tokens2024-11-05 – 2026-07-28clientMAYsampling“The client MAY choose to sample fewer tokens than the requested maximum.”https://modelcontextprotocol.io/specification/2026-07-28/schema#createmessagerequestparamsn/ayes2024-11-05/2025-03-26 wording “as requested by the server. The client MAY choose to sample fewer tokens than requested.”
sampling-client-may-ignore-temperature-stop-metadata2026-07-28clientMAYsampling“The client MAY modify or ignore temperature, stopSequences and metadata.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#sampling-parametersn/ayesnew prose
sampling-client-may-ignore-model-preferences2024-11-05 – 2026-07-28clientMAYsampling“The server’s preferences for which model to select. The client MAY ignore these preferences.”https://modelcontextprotocol.io/specification/2026-07-28/schema#createmessagerequestparamsn/ayes
sampling-client-may-modify-system-prompt2024-11-05 – 2026-07-28clientMAYsampling“The client MAY modify or omit this prompt.” (2026 prose adds “without communicating this to the server”)https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#system-promptn/ayes — daemon uses systemPrompt as the question’s message (hooks.go:194)2026 adds prose section
sampling-client-may-ignore-includecontext2024-11-05 – 2026-07-28clientMAYsampling“The client MAY modify or ignore this field without communicating this to the server.”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#context-inclusionn/ayes — mcpx has no context to includeschema-only until 2026
sampling-hints-evaluated-in-order2024-11-05 – 2026-07-28clientMUSTsampling“If multiple hints are specified, the client MUST evaluate them in order (such that the first match is taken).”https://modelcontextprotocol.io/specification/2026-07-28/schema#modelpreferencesyes — forward hints order unchangedyes — delegated
sampling-hints-over-priorities2024-11-05 – 2026-07-28clientSHOULDsampling“The client SHOULD prioritize these hints over the numeric priorities, but MAY still use the priorities to select from ambiguous matches.”https://modelcontextprotocol.io/specification/2026-07-28/schema#modelpreferencesn/ayes — delegated
sampling-hint-name-substring2024-11-05 – 2026-07-28clientSHOULDsampling“The client SHOULD treat this as a substring of a model name”https://modelcontextprotocol.io/specification/2026-07-28/schema#modelhintn/ayes — delegated
sampling-hint-may-map-provider2024-11-05 – 2026-07-28clientMAYsampling“Clients MAY map hints to equivalent models from different providers”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#model-hintsn/ayes
sampling-stopreason-custom-values2024-11-05 – 2026-07-28clientMAYsampling“implementations MAY provide their own arbitrary values” (schema: “open string”)https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#result-fieldsyes — must pass unknown stopReason throughyesprose added 2026; toolUse value added 2025-11-25
sampling-client-errors-user-rejected2024-11-05 – 2025-06-18clientSHOULDsampling“Clients SHOULD return errors for common failure cases” (example code -1, “User rejected sampling request”)https://modelcontextprotocol.io/specification/2025-06-18/client/sampling#error-handlingyes — downstream error must be relayed upstream as an erroryes — “a decline becomes an error, since the specification has no decline result for sampling” (docs/protocol.md §4.1)
sampling-client-errors-user-rejected-and-tool-shape2025-11-25clientSHOULDsampling“User rejected sampling request: -1 - Tool result missing in request: -32602 (Invalid params) - Tool results mixed with other content: -32602 (Invalid params)”https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#error-handlingyes (relay)yes — -1/-32602 not specifically produced; handler errors become -32603 (modern.go:125)removed in 2026-07-28
sampling-client-user-approval-controls2024-11-05 – 2026-07-28clientSHOULDsampling“Clients SHOULD implement user approval controls”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsn/ayes — broker policy/audience
sampling-both-validate-message-content2024-11-05 – 2026-07-28bothSHOULDsampling“Both parties SHOULD validate message content”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsyesyes
sampling-client-respect-hints2024-11-05 – 2026-07-28clientSHOULDsampling“Clients SHOULD respect model preference hints”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsn/ayes — delegated
sampling-client-rate-limit2024-11-05 – 2026-07-28clientSHOULDsampling“Clients SHOULD implement rate limiting”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsn/ayes — proto.samplingPerMinute (60) per server and proto.samplingSessionPerMinute (20) per session, fixed window; a refused request reaches no handler (#2192)
sampling-both-handle-sensitive-data2024-11-05 – 2026-07-28bothMUSTsampling“Both parties MUST handle sensitive data appropriately”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsyes — prompts/completions pass through mcpx and its logyes — same
sampling-both-iteration-limits-tool-loops2025-11-25 – 2026-07-28bothSHOULDsampling“Both parties SHOULD implement iteration limits for tool loops”https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#security-considerationsyes — proto.askRounds covers 2026 retriesyesnew
elicitation-server-no-sensitive-info2025-06-18serverMUST NOTelicitation“Servers MUST NOT use elicitation to request sensitive information.”https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#user-interaction-modelyes — mcpx cannot vet upstream content; forwards form questions as-isn/a (peer’s duty)narrowed to form mode in 2025-11-25
elicitation-server-no-sensitive-info-in-form-mode2025-11-25 – 2026-07-28serverMUST NOTelicitation“Servers MUST NOT use form mode elicitation to request sensitive information such as passwords, API keys, access tokens, or payment credentials” (restated: “Servers MUST NOT request sensitive information (passwords, API keys, etc.) via form mode”)https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#user-interaction-modelyes — pass-through of upstream’s form; mcpx’s own questions must complyn/a
elicitation-server-sensitive-via-url-mode2025-11-25 – 2026-07-28serverMUSTelicitation“Servers MUST use URL mode for interactions involving such sensitive information”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#user-interaction-modelyes — mcpx raises no url-mode question of its own (docs/protocol.md §6)n/anew in 2025-11-25
elicitation-app-show-requesting-server2025-06-18clientSHOULDelicitation“Applications SHOULD: - Provide UI that makes it clear which server is requesting information” (also “Clients SHOULD provide clear indication of which server is requesting information”)https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerationsyes — message rewritten “ (via mcpx) asks:” so the downstream can meet ityes — broker records Serverpromoted to MUST in 2025-11-25
elicitation-client-show-requesting-server2025-11-25 – 2026-07-28clientMUSTelicitation“Clients MUST provide clear indication of which server is requesting information” (and “MCP clients MUST: - Provide UI that makes it clear which server is requesting information”)https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerationsyes — rewrite above; without it the downstream would only see “mcpx”yes — delegatedwas SHOULD
elicitation-app-review-modify-responses2025-06-18clientSHOULDelicitation“Allow users to review and modify their responses before sending”https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#user-interaction-modeln/ayes — delegatedpromoted
elicitation-client-form-review-modify-responses2025-11-25 – 2026-07-28clientMUSTelicitation“For form mode, allow users to review and modify their responses before sending”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#user-interaction-modeln/ayes — delegated; broker auto-policy answers bypass review (see Implied)was SHOULD
elicitation-app-decline-cancel-options2025-06-18clientSHOULDelicitation“Respect user privacy and provide clear decline and cancel options”https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#user-interaction-modeln/ayes — mcpx elicit decline/cancelpromoted
elicitation-client-decline-cancel-options2025-11-25 – 2026-07-28clientMUSTelicitation“Respect user privacy and provide clear decline and cancel options”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#user-interaction-modeln/ayes — broker keeps decline ≠ cancel; expiry answers cancelwas SHOULD
elicitation-client-url-show-domain-and-consent2025-11-25 – 2026-07-28clientMUSTelicitation“For URL mode, clearly display the target domain/host and gather user consent before navigation to the target URL”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#user-interaction-modeln/ayes — mcpx declares form only upstream, so url mode should not arrive; broker/TUI must meet it if it doesnew
elicitation-client-declare-capability-at-initialize2025-06-18 – 2025-11-25clientMUSTelicitation“Clients that support elicitation MUST declare the elicitation capability during initialization”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#capabilitiesn/ayes — elicitation: {}replaced in 2026
elicitation-client-declare-capability-per-request-meta2026-07-28clientMUSTelicitation“Clients that support elicitation MUST declare the elicitation capability in _meta.io.modelcontextprotocol/clientCapabilities on each request”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#capabilitiesyes — mcpx must read the capability from this request’s _meta, not a prior oneyesnew
elicitation-empty-capability-means-form-only2025-11-25 – 2026-07-28both(definition)elicitation“For backwards compatibility, an empty capabilities object is equivalent to declaring support for form mode only”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#capabilitiesyes — CanElicit("url") requires elicitation.urlyes — mcpx’s {} therefore declares form onlynew
elicitation-client-support-at-least-one-mode2025-11-25 – 2026-07-28clientMUSTelicitation“Clients declaring the elicitation capability MUST support at least one mode (form or url).”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#capabilitiesn/ayes — formnew
elicitation-server-no-undeclared-modes2025-11-25 – 2026-07-28serverMUST NOTelicitation“Servers MUST NOT send elicitation requests with modes that are not supported by the client.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#capabilitiesyes — CanElicit gates url on elicitation.url and on revision; url questions otherwise stay with the brokern/anew
elicitation-server-may-request-during-client-request2026-07-28serverMAYelicitation“Servers MAY request information from a user during the processing of a client request, by sending an InputRequiredResult containing an elicitation/create request.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#elicitation-requestsyes — modern downstream gets input_requiredyes — resolveInputlegacy: a server-initiated request
elicitation-request-must-include-mode-and-message2025-11-25 – 2026-07-28serverMUSTelicitation“All elicitation requests MUST include the following parameters: … mode … Optional for form mode … message”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#elicitation-requestsyesn/aself-contradictory on mode (Conflicts)
elicitation-server-may-omit-mode-for-form2025-11-25 – 2026-07-28serverMAYelicitation“For backwards compatibility, servers MAY omit the mode field for form mode elicitation requests.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#elicitation-requestsyes — mcpx strips mode for 2025-06-18 clientsn/a
elicitation-client-missing-mode-is-form2025-11-25 – 2026-07-28clientMUSTelicitation“Clients MUST treat requests without a mode field as form mode.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#elicitation-requestsn/ayes — hooks.go:133 defaults to form
elicitation-form-request-shape2025-11-25 – 2026-07-28serverMUSTelicitation“Form mode elicitation requests MUST either specify mode: \"form\" or omit the mode field, and include these additional parameters: requestedSchema”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-elicitation-requestsyesn/a2025-06-18: message+requestedSchema required by schema only
elicitation-form-schema-flat-primitives2025-06-18 – 2026-07-28serverMUST (schema-structural)elicitation“form mode elicitation schemas are limited to flat objects with primitive properties only” / schema: “Only top-level properties are allowed, without nesting.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#requested-schemayes — pass-through; a 2025-06-18 downstream cannot parse default, oneOf, arrays (added 2025-11-25) — not downgraded by mcpx (unverified)n/a2025-11-25 adds defaults, titled/untitled single/multi-select enums; enumNames becomes LegacyTitledEnumSchema
elicitation-client-prepopulate-defaults2025-11-25 – 2026-07-28clientSHOULDelicitation“Clients that support defaults SHOULD pre-populate form fields with these values.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#requested-scheman/ayes — delegatednew (SEP-1034); keyword not bolded
elicitation-url-request-shape-with-id2025-11-25serverMUSTelicitation“URL mode elicitation requests MUST specify mode: \"url\", a message, and include these additional parameters: url … elicitationId”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-mode-elicitation-requestsyes — a url question from a 2026 upstream forwarded to a 2025-11-25 downstream needs a synthesized elicitationId (unverified that mcpx does this)n/aelicitationId removed in 2026-07-28
elicitation-url-request-shape2026-07-28serverMUSTelicitation“URL mode elicitation requests MUST specify mode: \"url\", a message, and include these additional parameters: url”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#url-mode-elicitation-requestsyes — paramsFor strips elicitationId only for clients older than 2025-11-25, so a 2025-11-25 upstream’s id reaches a 2026 downstream (extra field)n/achanged
elicitation-url-must-be-valid2025-11-25 – 2026-07-28serverMUSTelicitation“The url parameter MUST contain a valid URL.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#url-mode-elicitation-requestsyes — pass-throughn/a
elicitation-client-elicitationid-opaque2025-11-25clientMUSTelicitation“The ID of the elicitation, which must be unique within the context of the server. The client MUST treat this ID as an opaque value.”https://modelcontextprotocol.io/specification/2025-11-25/schema#elicitrequesturlparamsn/ayes — carried, not parsedremoved 2026
elicitation-server-may-send-complete-notification2025-11-25serverMAYelicitation“Servers MAY send a notifications/elicitation/complete notification when an out-of-band interaction started by URL mode elicitation is completed.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#completion-notifications-for-url-mode-elicitationyes — events.go:264 emits it downstreamyes — client.go:501 listensremoved 2026
elicitation-complete-only-to-initiating-client2025-11-25serverMUSTelicitation“MUST only send the notification to the client that initiated the elicitation request.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#completion-notifications-for-url-mode-elicitationyes — must route by the connection the question went to, not broadcast (unverified which)n/aremoved 2026
elicitation-complete-must-carry-id2025-11-25serverMUSTelicitation“MUST include the elicitationId established in the original elicitation/create request.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#completion-notifications-for-url-mode-elicitationyes — id must be the one the downstream sawn/aremoved 2026
elicitation-client-ignore-unknown-complete2025-11-25clientMUSTelicitation“MUST ignore notifications referencing unknown or already-completed IDs.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#completion-notifications-for-url-mode-elicitationn/ayesremoved 2026
elicitation-client-may-auto-retry-on-complete2025-11-25clientMAYelicitation“MAY wait for this notification to automatically retry requests that received a URLElicitationRequiredError”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#completion-notifications-for-url-mode-elicitationn/ayesremoved 2026
elicitation-client-manual-retry-cancel-controls2025-11-25 – 2026-07-28clientSHOULDelicitation“Clients SHOULD provide manual controls that let the user retry or cancel the original request (or otherwise resume interacting with the client).”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#example-request-sensitive-datan/ayes — mcpx elicit CLI/TUI2025-11-25: “SHOULD still provide … if the notification never arrives”
elicitation-server-may-return-url-required-error2025-11-25serverMAYelicitation“the server MAY return a URLElicitationRequiredError (code -32042)”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-erroryes — relay upstream -32042 only to a downstream that declared elicitation.url (unverified)yes — must parse -32042 data (unverified)removed 2026; code reserved
elicitation-url-required-error-only-when-required2025-11-25serverMUST NOTelicitation“The server MUST NOT return this error except when URL mode elicitation is required.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-erroryesn/aremoved 2026
elicitation-url-required-error-lists-elicitations2025-11-25serverMUSTelicitation“The error MUST include a list of elicitations that are required to complete before the original can be retried.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-erroryesn/aremoved 2026
elicitation-url-required-error-url-mode-with-id2025-11-25serverMUSTelicitation“Any elicitations returned in the error MUST be URL mode elicitations and have an elicitationId property.”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-erroryesn/aremoved 2026
elicitation-server-must-return-320422025-11-25serverMUSTelicitation“Servers MUST return standard JSON-RPC errors for common failure cases: - When a request cannot be processed until an elicitation is completed: -32042”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#error-handlingyesn/aconflicts with the MAY above (Conflicts)
elicitation-client-32602-on-undeclared-mode2025-11-25clientMUSTelicitation“Clients MUST return standard JSON-RPC errors … Server sends an elicitation/create request with a mode not declared in client capabilities: -32602 (Invalid params)”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#error-handlingn/ayes — an undeclared mode is -32602 (elicitModeDeclared, internal/mcpclient/modern.go); tested, see conformance-matrix.mdremoved 2026
elicitation-no-32042-in-20262026-07-28bothMUST NOTelicitation“Implementations of this protocol version MUST NOT emit these codes: … -32042 — URL elicitation required (2025-11-25 only).”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codesyes — must not relay a legacy upstream’s -32042 verbatim to a 2026 downstreamyesnew
elicitation-server-state-bound-to-user2025-11-25serverMUSTelicitation“Servers implementing elicitation MUST securely associate this state with individual users”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#statefulnessyes — broker binds questions to session/connectionn/a2026 makes it conditional
elicitation-server-stored-state-bound-to-user2026-07-28serverMUSTelicitation“if state is stored, servers implementing elicitation MUST securely associate this state with individual users”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#statefulnessyes — mcpx stores (broker + daemon task) so it binds; requestState carries connection idn/aconditional
elicitation-state-not-session-id-alone2025-11-25serverMUST NOTelicitation“State MUST NOT be associated with session IDs alone”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#statefulnessyes — GAP: mcpx binds to MCPX_SESSION_ID/connection, i.e. session identity alone (no user auth exists)n/aremoved 2026
elicitation-state-storage-protected2025-11-25 – 2026-07-28serverMUSTelicitation“State storage MUST be protected against unauthorized access”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#statefulnessyes — broker SQLite is local-user only; /v1/elicit over a non-loopback port is not protectedn/a
elicitation-remote-user-id-from-authorization2025-11-25 – 2026-07-28serverMUSTelicitation“For remote MCP servers, user identification MUST be derived from credentials acquired via MCP authorization when possible (e.g. sub claim)”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#statefulnessnot-implemented-by-choice — mcpx does no OAuth; “when possible” is not met on --address 0.0.0.0n/a
elicitation-server-must-not-self-authorize-via-url2025-11-25 – 2026-07-28serverMUST NOTelicitation“MCP servers MUST NOT rely on URL mode elicitation to authorize users for themselves.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#url-mode-elicitation-for-oauth-flowsyes — mcpx raises nonen/a
elicitation-third-party-creds-not-via-client2025-11-25 – 2026-07-28serverMUST NOTelicitation“The third-party credentials MUST NOT transit through the MCP client”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#understanding-the-distinctionyes — mcpx is in the path only for the URL, not the credentialyes
elicitation-no-token-passthrough2025-11-25 – 2026-07-28serverMUST NOTelicitation“The MCP server MUST NOT use the client’s credentials for the third-party service”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#understanding-the-distinctionnot-implemented-by-choice (no OAuth)n/a
elicitation-user-authorizes-server-directly2025-11-25 – 2026-07-28serverMUSTelicitation“The user MUST authorize the MCP server directly”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#understanding-the-distinctionyes — pass-through URL opens the upstream directlyn/a
elicitation-server-not-transmit-url-creds-to-client2025-11-25 – 2026-07-28serverMUST NOTelicitation“The MCP server MUST NOT transmit credentials obtained through URL mode elicitation to the MCP client.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#understanding-the-distinctionyesn/a
elicitation-server-handle-decline-cancel-failure2026-07-28serverSHOULD NOT / MUSTelicitation“Servers SHOULD NOT assume that elicitation requests will always succeed, and MUST handle cases where the user declines or cancels the elicitation, or where the client fails to process the request.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#error-handlingyes — mcpx as server re-asks up to askRounds / hands back canceln/areplaces 2025-11-25 error-code MUSTs
elicitation-server-bind-to-client-and-user2025-11-25 – 2026-07-28serverMUSTelicitation“Servers MUST bind elicitation requests to the client and user identity”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerationsyes — per-connection correlation; ambiguous shared scope goes to broker (docs/protocol.md §3.4)n/anew in 2025-11-25
elicitation-client-user-approval-controls2025-06-18 – 2026-07-28clientSHOULDelicitation“Clients SHOULD implement user approval controls”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerationsn/ayes — broker policy
elicitation-client-allow-decline-any-time2025-06-18 – 2026-07-28clientSHOULDelicitation“Clients SHOULD allow users to decline elicitation requests at any time”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerationsn/ayes
elicitation-client-rate-limit2025-06-18 – 2025-11-25clientSHOULDelicitation“Clients SHOULD implement rate limiting”https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#security-considerationsn/ayes — proto.elicitationPerMinute (30) per server, fixed window; a refused request reaches no handler and is not answered cancel (#2192)removed in 2026-07-28
elicitation-client-clear-what-and-why2025-06-18 – 2026-07-28clientSHOULDelicitation“Clients SHOULD present elicitation requests in a way that makes it clear what information is being requested and why”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerationsyes — forward message intact (only prefixed)yes — delegated
elicitation-both-validate-against-schema2025-06-18bothSHOULDelicitation“Both parties SHOULD validate elicitation content against the provided schema”https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerationsyesyessplit per side in 2025-11-25
elicitation-client-validate-responses2025-11-25 – 2026-07-28clientSHOULDelicitation“Clients SHOULD validate all responses against the provided schema”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-securityn/ayes — broker records problems on answers (routes_proto.go:446)
elicitation-server-validate-received-data2025-11-25 – 2026-07-28serverSHOULDelicitation“Servers SHOULD validate received data matches the requested schema”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-securityyes — mcpx as server should validate before relaying upstreamn/a
elicitation-url-no-sensitive-user-info2025-11-25 – 2026-07-28serverMUST NOTelicitation“MUST NOT include sensitive information about the end-user, including credentials, personally identifiable information, etc., in the URL”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyes — pass-through; mcpx must not append its own session/ids to the URLn/a
elicitation-url-not-pre-authenticated2025-11-25 – 2026-07-28serverMUST NOTelicitation“MUST NOT provide a URL which is pre-authenticated to access a protected resource”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyesn/a
elicitation-no-clickable-urls-in-form2025-11-25 – 2026-07-28serverSHOULD NOTelicitation“SHOULD NOT include URLs intended to be clickable in any field of a form mode elicitation request.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyes — the “(via mcpx)” prefix adds no URLn/a
elicitation-url-https-outside-dev2025-11-25 – 2026-07-28serverSHOULDelicitation“SHOULD use HTTPS URLs for non-development environments.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyes — pass-throughn/a
elicitation-client-handle-urls-carefully2025-11-25 – 2026-07-28clientMUSTelicitation“Clients implementing URL mode elicitation MUST handle URLs carefully to prevent users from unknowingly clicking malicious links.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes — if the TUI/plugin offers to open a URL (docs/elicitation.md §5.6)
elicitation-client-no-url-prefetch2025-11-25 – 2026-07-28clientMUST NOTelicitation“MUST NOT automatically pre-fetch the URL or any of its metadata.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyes — a proxy must not fetch/validate the URL in transit either (implied)yes
elicitation-client-no-open-without-consent2025-11-25 – 2026-07-28clientMUST NOTelicitation“MUST NOT open the URL without explicit consent from the user.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes — broker auto rules must never “accept” a url question (implied)
elicitation-client-show-full-url2025-11-25 – 2026-07-28clientMUSTelicitation“MUST show the full URL to the user for examination before consent.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingyes — URL must reach the downstream unmodifiedyes
elicitation-client-open-url-securely2025-11-25 – 2026-07-28clientMUSTelicitation“MUST open the URL provided by the server in a secure manner that does not enable the client or LLM to inspect the content or user inputs.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes — url questions must not route to the agent audience
elicitation-client-highlight-domain2025-11-25 – 2026-07-28clientSHOULDelicitation“SHOULD highlight the domain of the URL to mitigate subdomain spoofing.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes
elicitation-client-warn-punycode2025-11-25 – 2026-07-28clientSHOULDelicitation“SHOULD have warnings for ambiguous/suspicious URIs (i.e., containing Punycode).”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes
elicitation-client-no-clickable-except-url-field2025-11-25 – 2026-07-28clientSHOULD NOTelicitation“SHOULD NOT render URLs as clickable in any field of an elicitation request, except for the url field in a URL elicitation request”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handlingn/ayes
elicitation-server-no-trust-client-user-id2025-11-25 – 2026-07-28serverMUST NOTelicitation“Servers MUST NOT rely on client-provided user identification without server verification, as this can be forged.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#identifying-the-useryes — GAP-ish: MCPX_SESSION_ID is client-provided; the signed requestState binds to a connection mcpx issued, which is the verificationn/a
elicitation-server-follow-best-practices2025-11-25 – 2026-07-28serverSHOULDelicitation“Instead, servers SHOULD follow security best practices.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#identifying-the-useryesn/a
elicitation-url-verify-opening-user2025-11-25 – 2026-07-28serverMUSTelicitation“The MCP Server MUST verify the identity of the user who opens the URL before accepting information.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#phishingn/a — mcpx issues no URL; the upstream that does sees mcpx’s identity, not the end user’sn/a
elicitation-url-same-user-start-and-complete2025-11-25 – 2026-07-28serverMUSTelicitation“the server MUST ensure that the user who started the elicitation request … is the same user who completes the authorization flow.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#phishingn/a (see Implied: shared pooled upstream)n/a
elicitation-url-identity-resilient-to-url-tampering2025-11-25 – 2026-07-28serverMUSTelicitation“the server MUST ensure that the mechanism to determine the user’s identity is resilient to attacks where an attacker can modify the elicitation URL.”https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#phishingn/an/a
elicitation-task-related-id-shared2025-11-25serverMUSTelicitation“an elicitation that a task-augmented tool call depends on MUST share the same related task ID with that tool call’s task.”https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#associating-task-related-messagesyes — when mcpx forwards an upstream question for a task-augmented downstream call, _meta["io.modelcontextprotocol/related-task"] must name mcpx’s task id, not upstream’s (unverified)yes — must accept/ignore ittasks core only in 2025-11-25
mrtr-input-request-type-restricted2026-07-28serverMUSTmrtr“inputRequests values are request objects that MUST be one of ElicitRequest, CreateMessageRequest, or ListRootsRequest”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-basic-workflowyes — ping or custom upstream requests cannot be carried downstream in 2026yes — others answered -32601 in legacy; in 2026 no slot to refusenew
mrtr-server-reask-missing-rather-than-error2026-07-28serverSHOULDmrtr“the server SHOULD respond with a new InputRequiredResult requesting the missing information again, rather than returning an error.”https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#error-handlingyesn/a
mrtr-missing-capability-error-320212026-07-28serverMUSTerrors (roots/sampling/elicitation)“If processing a request requires a capability the client did not include in io.modelcontextprotocol/clientCapabilities, the server MUST return a MissingRequiredClientCapabilityError (-32021)”https://modelcontextprotocol.io/specification/2026-07-28/basic/index#metayes — conflicts with mcpx’s broker fallback when the downstream lacks elicitation (see Conflicts)yes — must surface -32021 from upstream (mcpx always declares elicitation, so should not see it for elicitation)new

Folded duplicates

Same requirement found by two sweeps with the same revision range; the second id was dropped in favour of the first.

droppedkept
stdio-fallback-not-keyed-to-one-codeversioning-fallback-not-keyed-to-one-code
streamable-http-client-sends-protocol-version-headerlifecycle-http-protocol-version-header
streamable-http-dual-era-client-detectversioning-dual-era-http-detect
cancellation-schema-server-stdio-only-listenstdio-server-cancelled-only-for-listen
mrtr-server-never-initiatesmessages-servers-must-not-initiate-requests
tools-x-mcp-header-client-rejectstreamable-http-client-rejects-invalid-x-mcp-header
tools-call-retry-new-idmrtr-new-id-on-retry
resources-updated-only-after-subscribesubscriptions-resources-updated-only-if-subscribed
resources-listen-no-unrequestedsubscriptions-only-requested-types
resources-listen-subscriptionidmeta-subscription-id-on-listen-notifications
meta-missing-required-rejectmeta-server-rejects-missing-required-field
meta-no-infer-capabilitiesmeta-server-no-infer-caps-from-prior
meta-no-rely-undeclared-capmeta-server-no-undeclared-client-capabilities
meta-missing-cap-errorerrors-missing-required-client-capability
meta-clientinfo-shouldmeta-client-should-send-clientinfo
meta-serverinfo-every-resultmeta-server-should-send-serverinfo
meta-info-not-for-behaviormeta-info-not-for-behavior-or-security
meta-resulttype-requiredmessages-resulttype-server-must-include
meta-resulttype-absent-is-completemessages-resulttype-absent-means-complete
cache-private-no-sharedcaching-private-no-cross-auth
discover-must-implementversioning-server-must-implement-discover
discover-dual-client-stdio-firstversioning-dual-era-client-probe-discover-first-stdio
mrtr-server-must-use-mrtr-for-client-featuresmrtr-server-requests-via-mrtr
mrtr-server-no-undeclared-input-requestsmrtr-only-declared-capabilities
mrtr-input-required-only-on-three-methodsmrtr-supported-requests-only
mrtr-server-must-not-assume-fulfilmentmrtr-no-assume-retry
mrtr-client-must-construct-inputs-before-retrymrtr-client-fulfil-before-retry

Pairs that look alike but cover different ranges were kept apart on purpose, because the requirement moved page in 2026-07-28 (timeouts moved from lifecycle to cancellation; stdio shutdown moved from lifecycle to transports/stdio).

Revision conflicts and spec-internal contradictions

WP6 output 1b. Everything where a message or behaviour legal in one revision is forbidden or required differently in another, plus places where one revision contradicts itself (page vs page, prose vs schema, changelog vs page). Per-area sections are the sweeps’ own lists, lightly re-headed; ids refer to requirements.md.

How mcpx chooses (#307)

Where a conflict below is not settled by the peer’s stated revision, mcpx is meant to decide it by spec.precedence (revisions in the order their rules win; default newest first, --mcp-spec <rev> moves one to the front) and spec.lenient (revisions not held strictly; default none), through internal/spec. See ../protocol.md §1.1.

Decided by those settings today:

  • Progress message to a 2024-11-05 client (2024-11-05’s schema has no message; every later revision does). Sent by default; stripped only when 2024-11-05 is first and strict. internal/mcpserver/relay.go.
  • Headline 2, server-initiated requests from a 2026-07-28 server. Dropped while 2026-07-28 is strict (default); answered when it is in spec.lenient. internal/mcpclient/client.go.

Every headline item now carries a disposition in the index below (Phase 1 of #2209): peer-settled (the peer’s stated revision decides it, in code), precedence-governed (decided by the settings above), a recorded reading (mcpx follows one named reading of contradictory spec texts), or undisposed (an open question is named instead of a silent choice). Per-area items in sections A-E are dispositioned in place (Phase 1 of #2209, complete): every item carries a disposition line of one of the four kinds above, and undisposed items name the specific open question instead of a silent choice.

Disposition index — headline items

  1. Recorded reading — on stdio send the cancel (MUST) and, for a graceful end, the result; on HTTP answer the listen request and close the stream. Reading recorded in the item and in the comment block at internal/mcpserver/listen.go.
  2. Precedence-governed — spec.Current().Governs("2026-07-28") at internal/mcpclient/client.go (server-initiated requests dropped while 2026-07-28 is strict; MRTR carriage in section E). Also in the “Decided” list above.
  3. Peer-settled — disconnect split by era at internal/mcpserver/server.go (disconnect SHOULD NOT cancel on legacy, MUST in 2026).
  4. Recorded reading — the legacy MUST governs legacy initialize: negotiate() counter-offers a supported version; -32022 applies to modern _meta-versioned requests only. See A/C5.
  5. Undisposed — open question (#1481 decision): the client choke keeps -32602 (internal/mcpclient/schemacheck.go, internal/daemon/callerr.go); should the serve seam era-split isError for >=2025-11-25 hosts while /v1 and the client API keep -32602?
  6. Peer-settled — era-split codes in code: legacy -32002 (internal/mcpserver/server.go, codeResourceNotFoundLegacy), 2026 -32602.
  7. Undisposed — open question: downgrade() (internal/mcpserver/revisions.go) deletes structuredContent only for pre-2025-06-18; a non-object value to a 2025-06-18..2025-11-25 (object-only) host passes unchanged. Render into content, wrap, or error? Check the conformance rows first.
  8. Peer-settled — nextPage (internal/mcpclient/client.go): a missing cursor ends results in every revision; in 2026 an empty string is a valid cursor, not the end.
  9. Undisposed — open question: the logging gate. The client relay gates on a requested level (internal/mcpclient/relay.go, moreVerbose); if no path relays upstream logs to a host that requested no level, this item is moot in the relay direction and should be closed saying so, with the site.
  10. Peer-settled — legacy session-404 re-initialisation is era-keyed in internal/mcpclient’s legacy session recovery (internal/mcpclient/client.go:1138-1145): a session-carrying 404 re-initialises on the legacy path, a modern-era 404 surfaces; on the server side an unknown method on the modern path is answered 404 with -32601 (internal/mcpserver/server.go:879). See section B item C7.
  11. Peer-settled — batchRefused (internal/mcpserver/transport.go): batches are refused outside 2025-03-26, the one revision that mandates receiving them.
  12. Recorded reading — the schema and caching page govern over the changelog’s list: server/discover results are CacheableResults, and mcpx implements it that way — the discover result (internal/mcpserver/server.go) emits supportedVersions, and the modern envelope adds ttlMs/cacheScope to it as to every cacheable result (server.go:762-763; covered by TestModernEnvelope).
  13. Undisposed — both halves stay open, with the actual behaviour now recorded for one: cancelled-task finality (core vs extension) has no verified site (section C item C11), and the missing-required-task code (-32601 vs -32600) is not implemented either way — a non-augmented call executes ordinarily (section C item C10, internal/mcpserver/tasks.go:237-245).

Headline items

  1. Subscription teardown, 2026-07-28 (three sources disagree). basic/patterns/cancellation: a server MUST send notifications/cancelled referencing the subscriptions/listen id when it tears the stream down, and MUST NOT send notifications/cancelled for any other purpose. basic/patterns/subscriptions: the server SHOULD send a successful subscriptions/listen response to signal a graceful end, and never mentions a cancel. schema.ts CancelledNotification: on stdio the server sends it, solely to end a listen stream; the Streamable HTTP page says notifications/cancelled is used only on stdio. Reading that satisfies all three: on stdio, send the cancel (MUST) and, for a graceful end, the result; on HTTP, answer the listen request and close the stream. That is the recorded reading, not current mcpx behaviour: closeListen/announceEnd (internal/mcpserver/listen.go:354-377) send both frames on every transport, including modern HTTP, so the HTTP side is an implementation gap against this reading, not settled behaviour. See C1 in section C.
  2. Server-initiated requests. Legal on every legacy revision (sampling, roots, elicitation, ping); forbidden in 2026-07-28, where they travel as MRTR inputRequests in a result.
  3. Disconnect as cancellation (Streamable HTTP). 2025-03-26..2025-11-25: a disconnect SHOULD NOT be treated as cancellation. 2026-07-28: MUST be.
  4. Legacy initialize with an unsupported version. Every legacy revision: the server MUST answer with a version it supports. -32022 exists only in 2026-07-28 and applies to _meta-versioned requests.
  5. Tool input validation errors. Protocol error up to 2025-06-18; isError tool result from 2025-11-25.
  6. Resource not found. -32002 (SHOULD) up to 2025-11-25; -32602 (MUST) in 2026-07-28.
  7. structuredContent / outputSchema. Object-only in 2025-06-18..2025-11-25; any JSON value / any 2020-12 schema in 2026-07-28.
  8. Empty-string cursor. Unspecified before; in 2026-07-28 MUST NOT be treated as end of results.
  9. Logging without a requested level. MAY send before 2026-07-28; MUST NOT after. logging/setLevel is removed in 2026-07-28 while logging is only deprecated.
  10. HTTP 404. Session ended (client MUST re-initialize) in 2025-03-26..2025-11-25; method not found in 2026-07-28.
  11. Batches. Receiving JSON-RPC batches MUST be supported in 2025-03-26 only; removed in 2025-06-18.
  12. server/discover cache hints. The 2026-07-28 changelog’s list of results needing ttlMs/cacheScope omits server/discover; the caching page and the schema require them on DiscoverResult.
  13. Tasks. 2025-11-25 core: a cancelled task stays cancelled; extension: may still end in another terminal status. Missing-required-task error code: -32601 on the tool-level rule vs -32600 in the error list. The changelog calls extension tasks unsolicited; the extension page forbids returning a task to a client that did not declare support.

A. Lifecycle, versioning, messages, errors, _meta, ping

C1. JSON-RPC batches: silent → MUST receive → removed.

  • Disposition: Peer-settled — batches are answered only for a 2025-03-26 peer and refused for every other revision (batchRefused / runBatch, internal/mcpserver/transport.go:106-116); see headline 11. The -32700 behaviour recorded below predates that split.
  • 2024-11-05: messages must “follow the JSON-RPC 2.0 specification” (which defines batches) but the schema’s JSONRPCMessage has no array form. https://modelcontextprotocol.io/specification/2024-11-05/basic/messages
  • 2025-03-26: “MCP implementations MAY support sending JSON-RPC batches, but MUST support receiving JSON-RPC batches.” https://modelcontextprotocol.io/specification/2025-03-26/basic/index#batching — and “The initialize request MUST NOT be part of a JSON-RPC batch … This also permits backwards compatibility with prior protocol versions that do not explicitly support JSON-RPC batches.” https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#initialization
  • 2025-06-18: changelog “Remove support for JSON-RPC batching” https://modelcontextprotocol.io/specification/2025-06-18/changelog#major-changes ; Batching section and JSONRPCBatchRequest deleted. Every later revision still says “MUST follow the JSON-RPC 2.0 specification” without excluding batches.
  • Effect: a batch is mandatory-to-accept only for 2025-03-26. A server serving 2025-03-26 and 2025-06-18 cannot know the version before parsing, so it has to accept arrays on the wire anyway (the version is inside the elements). mcpx rejects arrays (-32700).

C2. Request-ID uniqueness scope.

  • Disposition: Undisposed — open question: does mcpx’s client reuse request IDs after completion on a 2026-07-28 connection, and can a stale subscriptions/listen notification then collide with a new listen reusing the ID? No site verified in Phase 1; check request-ID allocation in internal/mcpclient.
  • 2024-11-05..2025-11-25: “The request ID MUST NOT have been previously used by the requestor within the same session.” https://modelcontextprotocol.io/specification/2025-11-25/basic/index#requests
  • 2026-07-28: “The request ID MUST NOT match the ID of any other request the sender has issued and not yet received a response for.” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requests
  • Reuse after completion is legal in 2026, illegal in legacy. Internal tension in 2026: changelog item 9 says a broken stream’s request must be re-issued “as a new request with a new request ID” (https://modelcontextprotocol.io/specification/2026-07-28/changelog#major-changes), and subscriptionId is “the JSON-RPC ID of the subscriptions/listen request” (schema NotificationMetaObject) — with reuse legal once a listen ends, a stale notification can carry an ID now used by a different listen.

C3. Server-initiated requests, including ping.

  • Disposition: Peer-settled — the peer’s era decides: legacy peers receive wire requests; a modern connection never does (server-initiated requests are governed/dropped per headline 2, internal/mcpclient/client.go), and methods a revision removed are answered -32601 (internal/mcpserver/revisions.go:139).
  • 2024-11-05..2025-11-25: requests go “from the client to the server or vice versa”; ping: “Either the client or server can initiate a ping” and “The receiver MUST respond promptly with an empty response”. https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping#behavior-requirements
  • 2026-07-28: “Servers MUST NOT initiate JSON-RPC requests” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/index ; changelog “Remove ping, logging/setLevel, and notifications/roots/list_changed.” https://modelcontextprotocol.io/specification/2026-07-28/changelog#major-changes
  • A 2026 client receiving ping has no defined behaviour; a 2026 server receiving ping would answer -32601 (unknown method), which a legacy client reads as a dead peer (the very bug mcpx fixed in its client).

C4. Error-response id when the request could not be read.

  • Disposition: Recorded reading — mcpx omits id on a parse-error response, following the 2025-11-25+ schema reading over JSON-RPC’s null and the legacy schema’s required non-null id; the legacy-schema invalidity is accepted and recorded here.
  • JSON-RPC 2.0 (imported by “MUST follow”): id “MUST be Null” when it could not be detected.
  • 2024-11-05..2025-06-18 schema: JSONRPCError { id: RequestId } (required, non-null); prose: “Responses MUST include the same ID as the request they correspond to.” GH(2025-06-18) / https://modelcontextprotocol.io/specification/2025-06-18/basic/index#responses — and “the ID MUST NOT be null” is stated for requests only.
  • 2025-11-25..2026-07-28: JSONRPCErrorResponse { id?: RequestId }, “(except in error cases where the ID could not be read due a malformed request)” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-responses
  • So a parse-error response is unrepresentable in the legacy schema (neither null nor absent allowed), null per JSON-RPC, and absent per 2025-11-25+ schema (which still admits no null). mcpx omits id — right for 2025-11-25+, schema-invalid for 2025-03-26/2025-06-18.

C5. Version negotiation: MUST counter-offer vs example error (lifecycle page, all legacy revisions).

  • Disposition: Recorded reading — the legacy MUST governs: negotiate() (internal/mcpserver/server.go) counter-offers a supported version; -32022 applies to modern _meta-versioned requests only. See headline 4.
  • Prose: “Otherwise, the server MUST respond with another protocol version it supports.” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
  • Same page, “Example initialization error”: "code": -32602, "message": "Unsupported protocol version", "data": {"supported": [...], "requested": "1.0.0"} https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#error-handling
  • The example is a server doing what the MUST forbids. 2026 then standardises that example’s shape under -32022 for modern requests only. mcpx now follows the legacy MUST: negotiate() (internal/mcpserver/server.go) counter-offers a supported version (its latest when the request is unknown or modern); -32022 is used only for modern _meta-versioned requests, as 2026 specifies. (When this item was recorded, mcpx returned -32022 on legacy initialize too.)

C6. Client on an unsupported negotiated version: SHOULD vs MUST (prose vs schema, 2024-11-05..2025-11-25).

  • Disposition: Undisposed — open question: when a server negotiates a version mcpx’s client cannot support, does the client disconnect (prose SHOULD) or treat disconnect as mandatory (schema MUST)? No site verified in Phase 1; check the client negotiation path in internal/mcpclient.
  • Prose: “If the client does not support the version in the server’s response, it SHOULD disconnect.” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
  • Schema InitializeResult.protocolVersion: “If the client cannot support this version, it MUST disconnect.” https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult
  • Unchanged in all four legacy revisions.

C7. Using un-negotiated capabilities: SHOULD → MUST (2025-06-18).

  • Disposition: Undisposed — open question: does mcpx’s server refuse a capability-gated method the peer never negotiated on a 2026-07-28 request (the MethodNotFoundError comment’s reading), or does its accept-liberally rule apply across eras? No site verified in Phase 1.
  • 2024-11-05, 2025-03-26: “Both parties SHOULD: … Only use capabilities that were successfully negotiated” https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#operation
  • 2025-06-18, 2025-11-25: “Both parties MUST: …” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#operation ; changelog “Change SHOULD to MUST in Lifecycle Operation”.
  • A server accepting an un-negotiated method (mcpx’s accept-liberally rule) is not clearly “using” a capability; a client calling one clearly is. The 2026 MethodNotFoundError comment (“or one gated behind a server capability the server did not advertise”) says the server should refuse such calls — https://modelcontextprotocol.io/specification/2026-07-28/schema#methodnotfounderror — contradicting accept-liberally for 2026.

C9. Reserved _meta prefixes changed incompatibly (2025-06-18 → 2025-11-25).

  • Disposition: Undisposed — open question: does mcpx validate or generate _meta prefixes per era, and would a 2025-06-18 peer’s legal io.modelcontextprotocol/ data be misread as protocol data? No site verified in Phase 1.
  • 2025-06-18: “Any prefix beginning with zero or more valid labels, followed by modelcontextprotocol or mcp, followed by any valid label, is reserved … modelcontextprotocol.io/, mcp.dev/, api.modelcontextprotocol.org/, and tools.mcp.com/ are all reserved.” https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
  • 2025-11-25+: “Any prefix where the second label is modelcontextprotocol or mcp is reserved … io.modelcontextprotocol/ … However, com.example.mcp/ is NOT reserved” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#meta
  • io.modelcontextprotocol/ — the prefix of every 2026 protocol key — is not reserved under the 2025-06-18 rule (nothing follows modelcontextprotocol), so a 2025-06-18 implementation could legally have used it for its own data. Conversely modelcontextprotocol.io/ and tools.mcp.com/ are reserved in 2025-06-18 and free in 2025-11-25.

C11. Error codes -32002 and -32042.

  • Disposition: Peer-settled — dual-era code choice is in code: legacy -32002 (codeResourceNotFoundLegacy, internal/mcpserver/server.go) vs 2026 -32602 (headline 6), and -32042 belongs to the 2025-11-25 era only — the client propagates it untouched (internal/mcpclient/client.go:630); the server-side return gap is recorded at section E, spec-internal item 2.
  • 2025-11-25 (and earlier for -32002): resource-not-found is -32002; URL_ELICITATION_REQUIRED = -32042 under “Implementation-specific JSON-RPC error codes [-32000, -32099]”. https://modelcontextprotocol.io/specification/2025-11-25/schema#urlelicitationrequirederror
  • 2026-07-28: “Implementations of this protocol version MUST NOT emit these codes: -32002 … -32042” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes
  • 2025-11-25 called -32000..-32099 wholly implementation-specific; 2026 carves -32020..-32099 out as spec-only. Any legacy SDK code in -32020..-32099 other than -32042 becomes forbidden to emit under 2026. A dual-era server must choose the code by the era of the request.

C15. Lifecycle requirement itself.

  • Disposition: Peer-settled — the dual-era server keys lifecycle off the request’s era: legacy initialize is served on the legacy path, while the modern path takes _meta-versioned requests and answers unknown/removed methods -32601 (internal/mcpserver/server.go:879, :916).
  • 2024-11-05..2025-11-25: “All implementations MUST support the base protocol and lifecycle management components.” https://modelcontextprotocol.io/specification/2025-11-25/basic/index
  • 2026-07-28: “All implementations MUST support the base protocol, versioning, and the message patterns.” — and initialize is an unknown method to a modern-only server. https://modelcontextprotocol.io/specification/2026-07-28/basic/index

C16. Where capabilities live, and whether they persist.

  • Disposition: Undisposed — open question: does any modern-path handler read capabilities stored from an earlier request on the same connection, contrary to the 2026 statelessness rule? Legacy session state is era-correct; no modern-path site verified in Phase 1.
  • Legacy: capabilities “establish which optional protocol features will be available during the session” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#capability-negotiation
  • 2026: “Servers MUST NOT rely on prior requests … (e.g., capabilities, protocol version, client identity)” and schema “Servers MUST NOT infer capabilities from prior requests.” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#statelessness
  • Remembering caps is required in one era and forbidden in the other; a dual-era server must key the rule off the request, not the connection.

C17. serverInfo placement and instructions.

  • Disposition: Recorded reading — mcpx follows the 2026 placement (see the item text): supportedVersions in the discover result, serverInfo in _meta, and ttlMs/cacheScope from the modern envelope.
  • Legacy: InitializeResult.serverInfo (required), instructions? top-level.
  • 2026: serverInfo only in _meta["io.modelcontextprotocol/serverInfo"] (SHOULD, every result); DiscoverResult.instructions top-level. https://modelcontextprotocol.io/specification/2026-07-28/server/discover#discoverresult
  • mcpx’s discover answer now uses the 2026 placement: supportedVersions in the result and serverInfo in _meta (added by the envelope to every modern result). The discover result also carries ttlMs/cacheScope, added by the modern envelope as for every cacheable result (headline 12).

C18. Timeouts relocated and reworded.

  • Disposition: Undisposed — open question: on a 2026-07-28 HTTP request, does mcpx’s sender cancel a timed-out request by closing the stream rather than sending notifications/cancelled? The relocation makes the two eras’ timeout behaviour differ; no timeout-path site verified in Phase 1.
  • 2025-03-26..2025-11-25 lifecycle: “the sender SHOULD issue a cancellation notification for that request” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#timeouts
  • 2026-07-28 cancellation page: “the sender SHOULD cancel the request and stop …” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#timeouts (cancellation owner has the details; the change matters because on HTTP 2026 cancellation is closing the stream, not a notification).

Within one revision

C8. “requests other than pings and logging” (2024-11-05..2025-11-25). “The server SHOULD NOT send requests other than pings and logging before receiving the initialized notification.” https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization — logging is notifications/message, a notification, so the exception is to a rule about requests. Read literally, notifications are unrestricted before initialized; read as intended, “messages” was meant and progress/list_changed notifications are discouraged.

  • Disposition: Recorded reading — the literal reading is recorded: the sentence restricts requests, so notifications before initialized are not restricted by it, whatever the drafters intended by “logging”.

C10. -32000..-32019: “legacy” (prose) vs “implementation-defined” (schema), 2026-07-28.

  • Disposition: Undisposed — open question: does mcpx emit any code in -32000..-32019 of its own on a 2026-07-28 request (prose: SHOULD NOT; schema/changelog: grandfathered)? No emissions audit done in Phase 1.
  • Prose: “-32000 to -32019 — legacy. … new implementations SHOULD NOT use codes from this sub-range at all.” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes
  • Schema comment: “-32000 to -32019: implementation-defined. Existing SDKs and implementations use codes here for their own purposes; the specification will never define codes in this sub-range” — no SHOULD NOT. Changelog item 12: “-32000 to -32019 remains implementation-defined (existing SDK usage is grandfathered)”. https://modelcontextprotocol.io/specification/2026-07-28/changelog#minor-changes
  • Also prose “Apart from -32002 … receivers MUST NOT assume any specific meaning” — but -32002 is in the legacy sub-range, and the same page tells clients to accept it as resource-not-found: a single spec-assigned meaning inside a range the schema says “the specification will never define codes in”.

C12. Request params optional (prose/type) vs required _meta (schema), 2026-07-28.

  • Disposition: Undisposed — open question: does mcpx’s client ever send a params-less 2026-07-28 request, and does its server reject a 2026 request whose _meta lacks the required fields? No site verified in Phase 1.
  • basic/index shows params?: { [key: string]: unknown } for requests. https://modelcontextprotocol.io/specification/2026-07-28/basic/index#requests
  • Schema RequestParams { _meta: RequestMetaObject } with required protocolVersion/clientCapabilities; “fields marked as required MUST be included on every request”. So every 2026 request has params, including ones legacy defined without params. The generic Request.params? is kept “to allow unofficial extensions”.

C13. Error for a legacy initialize at a modern-only server, 2026-07-28.

  • Disposition: Recorded reading — mcpx answers an unknown method on the modern path with -32601 (internal/mcpserver/server.go:879), i.e. the versioning matrix’s unknown-method reading, not the -32602 the malformed-request MUST picks.
  • basic/index: “A request missing any required field is malformed; the server MUST reject it with JSON-RPC error code -32602 (Invalid params). On HTTP, the response status MUST be 400 Bad Request.” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#meta
  • versioning matrix (Legacy/Modern): “the server rejects initialize with a JSON-RPC error; the exact code is implementation-defined (initialize is an unknown method and the request also lacks the required _meta fields).” https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#compatibility-matrix
  • stdio page: legacy servers answer pre-initialize probes “commonly -32601 or -32602”. Unknown-method (-32601) and malformed (-32602) are both reachable; the MUST picks one, the matrix says either.

C14. HTTP era detection: 400 vs 4xx, 2026-07-28.

  • Disposition: Undisposed — open question: which statuses trigger mcpx client era fallback — only 400 (binding page) or any 4xx without a recognised modern error body (matrix)? See also section B item C9.
  • Streamable HTTP: “On 400 Bad Request, the client SHOULD inspect the response body before falling back” https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#backward-compatibility
  • Versioning matrix (Dual-era/Legacy): “the modern request returns a 4xx without a recognized modern error body, and the client falls back” https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#compatibility-matrix
  • A legacy 2025-03-26..2025-11-25 server answering a session-less non-initialize POST commonly returns 400 or 404 (session required / unknown session); the binding page only covers 400.

C19. “Extensions … negotiated during initialization” in a revision with no initialization, 2026-07-28.

  • Disposition: Recorded reading — the versioning page governs over the index page’s sentence: extensions are negotiated per request via capabilities and server/discover, there being no initialization in 2026-07-28.
  • index.mdx: “Extensions are always opt-in and require explicit support from both client and server, negotiated during initialization.” https://modelcontextprotocol.io/specification/2026-07-28/index#extensions
  • versioning: extensions are advertised in capabilities, i.e. per request (clientCapabilities.extensions) and via server/discover. https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#extension-negotiation

C20. Architecture page vs statelessness, 2026-07-28.

  • Disposition: Recorded reading — the architecture page does not govern: servers declare features on server/discover, per-request declaration is the client’s, and sampling’s deprecation status is as in deprecated.mdx.
  • architecture: “servers declare their supported features on each request” https://modelcontextprotocol.io/specification/2026-07-28/architecture/index#capability-negotiation — servers declare on server/discover only; per-request declaration is the client’s.
  • architecture keeps “Built on JSON-RPC, MCP provides a protocol focused on context exchange and sampling coordination” while Sampling is Deprecated (deprecated.mdx).
  • architecture: “Receiving resource update notifications requires opening a subscriptions/listen stream” vs changelog: the server “acknowledges” — no server capability gate stated for update notifications, while ServerCapabilities.resources.subscribe still exists in schema.

C21. Notifications direction vs server-initiated cancellation, 2026-07-28.

  • Disposition: Recorded reading — covered by the headline 1 reading: the server-sent notifications/cancelled that terminates a subscriptions/listen stream is the one exception to the patterns page’s direction rule.
  • patterns/index: “The client sends JSON-RPC requests and notifications. The server answers each request with a JSON-RPC response … optionally preceded by notifications scoped to that request.” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/index
  • schema CancelledNotification: “On stdio, the server also sends this notification, solely to terminate a subscriptions/listen stream” and CancelledNotificationParams.requestId “MUST correspond to the ID of a request the client previously issued” — a server-sent cancel that refers to the client’s request, i.e. not “scoped to” a request it is answering but terminating it. (Cancellation/subscriptions owners hold the full version of this.)

C22. resultType unknown-value rule vs open type, 2026-07-28.

  • Disposition: Undisposed — open question: how does mcpx’s client treat an unrecognised resultType, and an absent one on a response to a request that carried protocolVersion: 2026-07-28? No site verified in Phase 1.
  • basic/index: “A resultType of any value unrecognized by the client MUST be considered invalid.” https://modelcontextprotocol.io/specification/2026-07-28/basic/index#resulttype
  • schema: type ResultType = "complete" | "input_required" | string; and “Servers implementing this protocol version MUST include this field. For backward compatibility, when a client receives a result from a server implementing an earlier protocol version … the client MUST treat the absent field as \"complete\".” A client cannot tell a pre-2026 server from a 2026 server that forgot the field, and on a response to a request that carried protocolVersion: 2026-07-28 the server by definition is not “implementing an earlier protocol version” — yet the only rule given is absent ⇒ complete.

C23. server/discover field names: versioning page vs mcpx (not a spec-internal conflict; recorded because it breaks interop). Spec: supportedVersions (schema DiscoverResult, discover.mdx, versioning page text “learn the server’s supported versions”). When recorded, mcpx server and client both used protocolVersions and top-level serverInfo. Fixed: the server emits supportedVersions (internal/mcpserver/server.go, server/discover case) with serverInfo in _meta, and the client parses supportedVersions (internal/mcpclient/probe.go). No longer an interop break; kept as the record of one.

  • Disposition: Recorded reading — resolved by fixing mcpx to the spec’s names (see the item text): the server emits supportedVersions (internal/mcpserver/server.go) and the client parses it (internal/mcpclient/probe.go). Kept as the record of the break.

B. Transports and authorization

C1. Server-to-client requests / client-to-server responses: legal ≤2025-11-25, forbidden 2026-07-28.

  • Disposition: Peer-settled — frame legality is keyed to the connection’s era in code: a modern connection carries no server-initiated wire requests (see section A item C3), so the forbidden frames cannot arise on it; legacy connections permit them.
  • 2025-11-25: “The server MAY send JSON-RPC requests and notifications before sending the JSON-RPC response.” — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server ; POST body “MUST be a single JSON-RPC request, notification, or response” (same anchor).
  • 2026-07-28: “The server MUST NOT send independent JSON-RPC requests on this stream.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#receiving-messages ; “The client MUST NOT send JSON-RPC responses.” — #sending-messages ; stdio: “The server MUST NOT write JSON-RPC requests to stdout.” / “The client MUST NOT write JSON-RPC responses.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#receiving-messages
  • Consequence: on one mcpx endpoint/process, whether a frame is legal depends on the connection’s era, not the endpoint.

C2. JSON-RPC batching: required-to-accept in 2025-03-26, removed 2025-06-18.

  • Disposition: Peer-settled — same site as section A item C1: batchRefused / runBatch (internal/mcpserver/transport.go:106-116); batches are received for 2025-03-26 only.
  • 2025-03-26 stdio: “Messages may be … a JSON-RPC batch” — https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdio ; HTTP: “An array batching one or more requests and/or notifications” — #sending-messages-to-the-server
  • 2025-06-18: “Messages are individual JSON-RPC requests, notifications, or responses.” ; “MUST be a single JSON-RPC request, notification, or response.” — https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio . Changelog: “Remove support for JSON-RPC batching (PR #416)”.
  • 2024-11-05 transports page is silent on batching (JSON-RPC 2.0 allows it).

C3. Relatedness of messages on a POST’s SSE stream: SHOULD → MUST.

  • Disposition: Undisposed — open question: does mcpx’s server scope every notification on a POST’s SSE stream to the originating request on a 2026-07-28 connection (MUST), and only by default on legacy (SHOULD)? No site verified in Phase 1.
  • 2025-11-25: “These messages SHOULD relate to the originating client request.” (#sending-messages-to-the-server)
  • 2026-07-28: “These notifications MUST relate to the originating client request.” (#receiving-messages). Unrelated server messages have no home except subscriptions/listen.

C4. Closing a POST SSE stream before the response.

  • Disposition: Undisposed — open question: per era, when may mcpx’s server close a POST SSE stream before the response — is the 2025-11-25 connection/stream distinction implemented, and is early close on 2026 treated as losing the request? No site verified in Phase 1.
  • 2025-03-26..2025-06-18: “The server SHOULD NOT close the SSE stream before sending the JSON-RPC response … unless the session expires.” — https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server
  • 2025-11-25: “the server MAY close the connection (without terminating the SSE stream) at any time” (after an event id) — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server . Legal only via the connection/stream distinction, which requires resumability.
  • 2026-07-28: no resumability, and closing the stream is cancellation (C6), so a server can no longer close early without losing the request.

C5. Resumability / Last-Event-ID / event IDs: optional 2025-03-26..2025-11-25 (with priming SHOULD in 2025-11-25), removed 2026.

  • Disposition: Undisposed — legacy resumability is implemented on the client (GET resume with Last-Event-ID, internal/mcpclient/http.go:332, :407); open question: does the modern path ignore Last-Event-ID as the 2026 backcompat text prescribes? No modern-path site verified in Phase 1.
  • 2025-11-25: “The server SHOULD immediately send an SSE event consisting of an event ID and an empty data field” ; “Resumption is always via HTTP GET with Last-Event-ID.” — #resumability-and-redelivery
  • 2026-07-28: “Resumable SSE streams via Last-Event-ID are not supported.” (#receiving-messages); modern-only server “SHOULD” ignore Last-Event-ID (#earlier-streamable-http-revisions).

C6. Disconnect semantics inverted.

  • Disposition: Peer-settled — the era split is in code (headline 3): in a session a dropped connection is not a cancellation (legacy SHOULD NOT; the client cancels explicitly), while a sessionless disconnect is treated as the cancellation signal (internal/mcpserver/server.go, session disconnect block).
  • 2025-03-26..2025-11-25: “Disconnection SHOULD NOT be interpreted as the client cancelling its request. To cancel, the client SHOULD explicitly send an MCP CancelledNotification.” — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
  • 2026-07-28: “Closing the SSE response stream MUST be treated by the server as cancellation of that request.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellation ; “on Streamable HTTP, closing the SSE response stream is itself the cancellation signal and no notifications/cancelled message is expected” (#sending-messages Note); cancellation page: “The server MUST treat a client disconnect as cancellation of that request.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#transport-specific-cancellation
  • Spec-internal (2026): the streamable-http page ties cancellation to “Closing the SSE response stream”; the cancellation page to “a client disconnect” — the latter also covers application/json responses, which have no SSE stream. A dual-era server must key the disconnect rule to the request’s era.

C7. HTTP 404 meaning.

  • Disposition: Peer-settled — era-keyed on both sides: the client recovers a session-carrying 404 by re-initialising only on the legacy path, and surfaces a modern-era 404 (internal/mcpclient/client.go:1138-1145); the server answers an unknown method on the modern path with 404 + -32601 (internal/mcpserver/server.go:879). See headline 10.
  • 2025-03-26..2025-11-25: 404 = session terminated; “When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session” — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management ; backcompat: 404 on POST ⇒ try HTTP+SSE GET (#backwards-compatibility).
  • 2026-07-28: 404 = unknown method: “it MUST respond with 404 Not Found and a JSON-RPC error with code -32601” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-header . A 2025-11-25 client that still holds a session id and gets this 404 is required to re-initialize; a 2025-11-25-rule client probing with POST falls to HTTP+SSE. Only the 2026 rule (“and the response body is not a recognized modern JSON-RPC error”) disambiguates.
  • Also a change against JSON-RPC-over-HTTP practice in 2025-*, where a method-not-found error was an ordinary response (status not specified; in practice 200).

C8. Missing MCP-Protocol-Version header.

  • Disposition: Undisposed — open question: does mcpx assume 2025-03-26 for a header-less legacy request (2025-06-18..2025-11-25 SHOULD) and reject a header-less 2026 request as a header mismatch (-32020)? No site verified in Phase 1.
  • 2025-06-18..2025-11-25: server “SHOULD assume protocol version 2025-03-26” — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header
  • 2026-07-28: “MAY treat a request that omits the header as protocol version 2025-03-26. A server that does not support such clients MUST reject a request without the header” (#protocol-version-header); Server Validation lists a missing MCP-Protocol-Version as a HeaderMismatch failure (#server-validation) → 400/-32020. Also 2025-11-25: “invalid or unsupported” header → 400 with no body format; 2026: unsupported → 400 + UnsupportedProtocolVersionError, mismatch → 400 + HeaderMismatch.
  • 2025-06-18 header rule applies to “all subsequent requests” (after initialize); 2026: “Every POST request”.

C9. HTTP+SSE fallback trigger narrowed twice.

  • Disposition: Undisposed — open question: which status/body combinations make mcpx’s client fall back to HTTP+SSE against each era’s rule, including the 2026 recognised-modern-error-body condition? No site verified in Phase 1.
  • 2025-03-26..2025-06-18: “If it fails with an HTTP 4xx status code (e.g., 405 Method Not Allowed or 404 Not Found)” — https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#backwards-compatibility
  • 2025-11-25: “"400 Bad Request", "404 Not Found" or "405 Method Not Allowed"” — https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#backwards-compatibility (401/403 no longer trigger it — matters for OAuth-protected servers).
  • 2026-07-28: those codes “and the response body is not a recognized modern JSON-RPC error” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#httpsse-transport-2024-11-05 . A 2025-11-25-rule client probing a modern-only 2026 server gets 400 (missing headers → HeaderMismatch) and then GETs, gets 405, and fails with a misleading diagnosis.

C10. Re-issue-after-broken-stream MUST lives only in the changelog.

  • Disposition: Undisposed — open question: does mcpx’s client re-issue a request lost to a broken modern stream under a new request ID, and does it guard non-idempotent tools/call re-issue? No site verified in Phase 1.
  • Changelog major 9: “clients MUST re-issue it as a new request with a new request ID” — https://modelcontextprotocol.io/specification/2026-07-28/changelog
  • Transport page only: “Resumable SSE streams via Last-Event-ID are not supported.” — no MUST; stdio page: “in-flight requests are simply lost and the client can retry them” (no keyword) — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio#unexpected-termination . Also re-issuing a non-idempotent tools/call is not qualified anywhere.

C11. Notification POSTs in 2026 have no defined header requirements, yet all POSTs need them.

  • Disposition: Undisposed — open question: what header requirements does mcpx apply to notification POSTs from a 2026-07-28 peer, given the revision defines none? No site verified in Phase 1.
  • “The client MUST include the request metadata headers on each POST request.” / “Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header.” vs Note: “header requirements for notification POSTs are not defined by this revision.” and Mcp-Method “Required For: All requests” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messages . The same Note says no client→server notifications exist on HTTP in 2026 at all, while the rules for accepting them (202) remain.

C12. Server-sent notifications/cancelled for subscription teardown: scope disagreement (2026-07-28).

  • Disposition: Recorded reading — the headline 1 reading governs: on stdio the server sends notifications/cancelled and, for a graceful end, the listen result; on HTTP it answers the listen request and closes the stream. See section C item C1. Implementation gap: mcpx does not follow that transport split — announceEnd (internal/mcpserver/listen.go:365-377) sends notifications/cancelled and then the listen result unconditionally, whatever the transport (for a modern HTTP stream the listen stream’s send is the HTTP SSE sender, listen.go:183).
  • schema.ts CancelledNotification: “On stdio, the server also sends this notification, solely to terminate a subscriptions/listen stream” — https://modelcontextprotocol.io/specification/2026-07-28/schema#cancellednotification
  • cancellation page: “A server MUST send notifications/cancelled referencing a subscriptions/listen request ID when it tears down that subscription stream” (no transport qualifier) — https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation
  • schema.ts SubscriptionsListenResult: “signalling that the subscription has ended gracefully … this result is sent only when the server tears the subscription down” — so graceful teardown has two mandated signals (a result response and a cancelled notification), and on stdio the notification is also the only server→client message the 2026 index’s “server-sent responses and notifications” permits beyond responses. The stdio page’s list of “three kinds of messages” the server writes does not include a server-sent notifications/cancelled. (Detail belongs to the subscriptions/cancellation owner; recorded here because it is transport-scoped.)

C13. MCP-Protocol-Version header in 2025-03-26 authorization before the transport defines it.

  • Disposition: Undisposed — open question (spec-internal/historical): does mcpx send MCP-Protocol-Version on authorization discovery requests while omitting it on 2025-03-26 MCP requests, as that era’s own pages imply? mcpx’s discovery surface was not audited in Phase 1.
  • 2025-03-26 authorization: “MCP clients SHOULD include the header MCP-Protocol-Version: <protocol-version> during Server Metadata Discovery” with example MCP-Protocol-Version: 2024-11-05 — https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery-headers
  • 2025-03-26 transports page defines no such header; it arrives in 2025-06-18 (#protocol-version-header), whose backcompat rule assumes a header-less request is 2025-03-26 — i.e. a 2025-03-26 client following its own auth page sends a header on discovery but not on MCP requests.

C14. Dynamic Client Registration: SHOULD → MAY → deprecated.

  • Disposition: Undisposed — open question: does mcpx implement Dynamic Client Registration at all, and if so under which era’s strength? Its authorization surface was not audited in Phase 1.
  • 2025-03-26..2025-06-18: “SHOULD support the OAuth 2.0 Dynamic Client Registration Protocol” — https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration
  • 2025-11-25: “MAY support” — https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration
  • 2026-07-28: “Dynamic Client Registration is deprecated.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#dynamic-client-registration . Yet 2026 adds a new MUST inside it (application_type).
  • 2026 security-considerations still links the proxy-consent MUST to “dynamically registered client” — a MUST scoped to a deprecated mechanism.

C15. Where the authorization server lives / discovery.

  • Disposition: Undisposed — open question: which discovery rule does mcpx’s client follow per era — 2025-03-26 default paths or Protected Resource Metadata with path-inserted well-known URIs? Not audited in Phase 1.
  • 2025-03-26: MCP server is (or fronts) the AS; “authorization base URL MUST be determined … by discarding any existing path component”; default /authorize, /token, /register fallbacks MUST be used — https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#authorization-base-url
  • 2025-06-18+: MCP server is a resource server only; clients “MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery” and no default-path fallback exists — https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#overview . 2025-11-25+ uses path-inserted well-known URIs (keeps the path the 2025-03-26 rule discarded).

C16. Token acceptance subject.

  • Disposition: Undisposed — open question: where does mcpx validate token audience — authorization server or resource server — for each era? Not audited in Phase 1.
  • 2025-06-18: “Authorization servers MUST only accept tokens that are valid for use with their own resources.” — https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-handling
  • 2025-11-25+: “MCP servers MUST only accept tokens that are valid for use with their own resources.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handling

C17. WWW-Authenticate on 401.

  • Disposition: Undisposed — open question: does mcpx’s server emit WWW-Authenticate on 401, publish Protected Resource Metadata at the well-known URI, or both? Not audited in Phase 1.
  • 2025-06-18: “MCP servers MUST use the HTTP header WWW-Authenticate when returning a 401 Unauthorized” — https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location
  • 2025-11-25+: “MCP servers MUST implement one of the following discovery mechanisms” (header or well-known) — https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements . A 2025-06-18 client that only parses the header fails against a well-known-only 2025-11-25 server.

C18. Who accumulates scopes on step-up.

  • Disposition: Undisposed — open question: on an insufficient-scope step-up, does mcpx’s client union previously granted scopes itself (2026 rule) or rely on the server including them (2025-11-25 recommendation)? Not audited in Phase 1.
  • 2025-11-25 (server side): “Recommended approach: Include both existing relevant scopes and newly required scopes to prevent clients from losing previously granted permissions” ; client step 2 uses the Scope Selection Strategy — https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors
  • 2026-07-28: “servers are not required to include the client’s previously granted scopes” ; “Scope accumulation across operations is a client-side responsibility” ; client step 2: “computing the union of the client’s previously requested scope set and the scopes from the current challenge” — https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#runtime-insufficient-scope-errors . A 2025-11-25 client against a 2026 server that emits per-operation scopes drops earlier grants.

C19. Sessions: mintable 2025-03-26..2025-11-25, removed 2026 — while 2026 still scopes legacy state “to the session (HTTP)”.

  • Disposition: Peer-settled — sessions exist only on the legacy path in code (session minting, session-scoped state, and the session disconnect rule, internal/mcpserver/server.go); the modern path is per-request and stateless.
  • 2026 changelog: “Remove protocol-level sessions and the Mcp-Session-Id header” ; streamable-http: modern-only server “ignore it, and do not mint or echo session IDs” (#earlier-streamable-http-revisions).
  • 2026 versioning: “An initialize request selects legacy semantics, scoped to the stdio process (stdio) or the session (HTTP)” — https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#backward-compatibility-with-initialization-based-versions . Not a contradiction for dual-era servers (they implement the legacy page too), but the 2026 transport page defines no session, so “the session (HTTP)” is defined only by the 2025-11-25 page it links.

C20. “Supports both POST and GET” satisfied by 405. 2025-* says the endpoint “MUST … support both POST and GET” (#streamable-http) and, two sections later, that the server may “return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE stream” (#listening-for-messages-from-the-server). Spec-internal; the practical reading is that 405 is a conforming answer to GET.

  • Disposition: Recorded reading — as the item states, the practical reading is that 405 is a conforming answer to GET; “support both” is read through the listening section, not against it.

C21. Header name casing drift. 2025-03-26..2025-06-18 Mcp-Session-Id; 2025-11-25 MCP-Session-Id; 2026 backcompat text back to Mcp-Session-Id. Harmless (2026: header names “MUST use case-insensitive comparisons”) but any implementation that string-compares header names breaks.

  • Disposition: Recorded reading — header names are compared case-insensitively (the 2026 rule, and Go net/http header handling throughout mcpx), so the casing drift is harmless here.

C. Cancellation, progress, pagination, logging, completion, tasks, caching, MRTR, subscriptions

C1 — 2026-07-28 server-initiated subscription teardown: three texts, three answers (spec-internal).

  • Disposition: Recorded reading — headline 1: on stdio send notifications/cancelled (the MUST), then the listen result for a graceful end (the SHOULD); on HTTP send the listen result and close the stream. Implementation gap (not settled behaviour): mcpx does not implement that transport split — the comment block at internal/mcpserver/listen.go (closeListen, :354) records honouring the MUST first and the result second with no transport qualifier, and announceEnd (:365-377) sends both frames unconditionally; for a modern HTTP stream the listen stream’s send is the HTTP SSE sender (listen.go:183), so mcpx sends notifications/cancelled on HTTP too, following the cancellation page’s unqualified MUST over the schema’s stdio scoping.
  • Cancellation page, intro (no heading): “A server MUST send notifications/cancelled referencing a subscriptions/listen request ID when it tears down that subscription stream (see [Subscriptions][subscriptions]). Servers MUST NOT send notifications/cancelled for any other purpose.” and Behavior Requirements: “Server-sent cancellation notifications MUST reference a subscriptions/listen request, to terminate that subscription stream” — https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation , …#behavior-requirements
  • Subscriptions page, Cancellation: “The server tears it down (e.g., during shutdown) — it SHOULD send a successful subscriptions/listen response to signal a graceful end (see Graceful Closure), then close the stream.” and Graceful Closure: “When the server ends a subscription on its own initiative (for example, during shutdown), it SHOULD respond to the original subscriptions/listen request with a completion result before closing the stream.” It never mentions a server-sent notifications/cancelled, and ends “See [Cancellation][cancellation] for the full rules.” — https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions#cancellation , #graceful-closure
  • Schema CancelledNotification: “On stdio, the server also sends this notification, solely to terminate a subscriptions/listen stream … Servers MUST NOT use this notification to cancel any other request.” — scopes the server-sent cancel to stdio; the prose MUST has no transport qualifier. https://modelcontextprotocol.io/specification/2026-07-28/schema#cancellednotification
  • Streamable HTTP note: “The only client-sent notification in the core protocol, notifications/cancelled, is used only on the stdio transport” — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messages-to-the-server (heading before “Receiving Messages”; anchor unverified).
  • Unresolved questions: (a) MUST cancel, SHOULD respond, or both? Doing both means sending notifications/cancelled for request id N and a result for id N, while the same cancellation page tells a receiver of a cancellation that the canceller “SHOULD ignore any response … that arrives afterward” — here the canceller and responder are the same party. (b) On HTTP, is the server-sent cancel required (prose) or not sent at all (schema “On stdio”)? (c) The SubscriptionsListenResult doc says it is “sent only when the server tears the subscription down”, consistent with the subscriptions page, not with the cancellation page. Safest reading for an implementer: on stdio send notifications/cancelled (satisfies the MUST) then the listen result (satisfies the SHOULD, and the client can tell graceful from abrupt); on HTTP send the listen result and close the SSE stream. mcpx today does the stdio behaviour on every transport (see the disposition above) — recorded as an implementation gap against this reading, not as the reading being implemented.

C2 — server-sent requests and server-sent cancellation: legal ≤2025-11-25 → forbidden 2026-07-28.

  • Disposition: Peer-settled — the server’s own notifications/cancelled for a timed-out elicitation/create (internal/mcpserver/conn.go:396) can only fire on a legacy-era connection, because a modern connection never carries the wire request it would cancel (see the item text).
  • ≤2025-11-25: “Either side can send a cancellation notification to indicate that a previously-issued request should be terminated.” https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
  • 2026: “Servers MUST NOT send notifications/cancelled for any other purpose.” (above), and “Servers MUST NOT initiate JSON-RPC requests” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns ; “The previous pattern of server-initiated requests is no longer supported. This is a breaking change.” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
  • Consequence for a dual-era server (mcpx): conn.go:265 (cancel own timed-out elicitation/create) must stay reachable only on legacy-era connections. It is today, because a modern connection never gets a wire request.

C3 — 2026-07-28 client cancellation on HTTP (spec-internal).

  • Disposition: Recorded reading — era-split in code: as a client mcpx POSTs notifications/cancelled on stdio and legacy-era HTTP, but for a 2026-07-28 HTTP upstream it sends no notification — cancelled() returns early when the transport is an HTTPTransport with a meta version set (internal/mcpclient/modern.go:582), so closing the response stream is the cancellation signal there, as the transport section describes; notification cancellation remains for the other transports and eras. TestModernHTTPCancellationClosesTheStream (internal/mcpclient/http_conformance_test.go) verifies no cancellation notification is POSTed for a modern HTTP upstream.
  • “A client SHOULD send a cancellation notification to indicate that a request it previously issued should be terminated.” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation
  • vs “Streamable HTTP: Closing the SSE response stream is the cancellation signal. … No notifications/cancelled message is required or expected.” …#transport-specific-cancellation, and the streamable-http note that notification POST header requirements “are not defined by this revision”.
  • The intro SHOULD is unqualified; the transport section contradicts it on HTTP. mcpx’s code splits by era: no notification on a 2026-07-28 HTTP upstream (stream close carries the cancellation, internal/mcpclient/modern.go:582); the notification POST remains for stdio and legacy-era HTTP.

C4 — HTTP disconnect meaning: “not cancellation” (SHOULD NOT) 2025-03-26..2025-11-25 → “is cancellation” (MUST) 2026-07-28.

  • Disposition: Peer-settled — same era split as section B item C6 / headline 3 (internal/mcpserver/server.go, session disconnect block).
  • 2025-11-25: “Disconnection SHOULD NOT be interpreted as the client cancelling its request.” / “To cancel, the client SHOULD explicitly send an MCP CancelledNotification.” https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server (same text 2025-03-26, 2025-06-18).
  • 2026: “The server MUST treat a client disconnect as cancellation of that request.” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#transport-specific-cancellation ; “Closing the SSE response stream MUST be treated by the server as cancellation” https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#cancellation
  • A dual-era HTTP server must pick behaviour by the request’s era.

C5 — empty cursor: allowed to mean “end” ≤2025-11-25 → forbidden 2026-07-28.

  • Disposition: Peer-settled — headline 8: nextPage (internal/mcpclient/client.go) treats a missing cursor as the end in every revision and an empty-string cursor as a valid cursor in 2026-07-28.
  • ≤2025-11-25 only says “Treat a missing nextCursor as the end of results” (an empty-string cursor was unaddressed; common SDK practice treats "" as end).
  • 2026: “an empty string is a valid cursor and thus MUST NOT be treated as the end of results” https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination#implementation-guidelines . Also 2026 drops “Don’t persist cursors across sessions” (no sessions).

C6 — who may send progress: either side ≤2025-11-25 → server only 2026-07-28.

  • Disposition: Peer-settled — follows section C item C2: a client has incoming requests to report progress on only in the legacy era; in 2026 progress is server-sent only.
  • ≤2025-11-25: “Either side can send progress notifications” https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress
  • 2026: “The server MAY send progress notifications to report the status of requests the client has issued.” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress . Follows from C2 (a client has no incoming requests to report on).

C7 — progress monotonicity strength (prose vs schema, all revisions).

  • Disposition: Recorded reading — as the item states, the prose MUST governs over the schema’s lowercase “should”.
  • Prose: “The progress value MUST increase with each notification” https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress#progress-flow
  • Schema: “The progress thus far. This should increase every time progress is made, even if the total is unknown.” https://modelcontextprotocol.io/specification/2026-07-28/schema#progressnotificationparams . Lowercase, not RFC — treat prose as governing.

C8 — log messages without a requested level: MAY ≤2025-11-25 → MUST NOT 2026-07-28.

  • Disposition: Undisposed — headline 9: the client relay gates on a requested level (moreVerbose, internal/mcpclient/relay.go); whether any path relays upstream logs to a host that requested no level — which would settle this item as moot in the relay direction — is the open question named there.
  • ≤2025-11-25 schema: “If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically.” https://modelcontextprotocol.io/specification/2025-11-25/schema#loggingmessagenotification
  • 2026: “The server MUST NOT emit notifications/message for a request that does not include this field.” https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#per-request-log-level . Also session-wide → per-request, and logging/setLevel removed (changelog 5). Invalid level: ≤2025-11-25 rejects the setLevel call; 2026 “SHOULD reject that request” — i.e. the tools/call (or whatever) carrying the bad _meta.
  • mcpx docs/protocol.md §2.2 says logging is “removed in 2026-07-28”; the spec deprecates it (still in schema ServerCapabilities.logging, still has a MUST-declare rule). Only logging/setLevel is removed.

C9 — completions capability: absent 2024-11-05 → MUST declare 2025-03-26+. A 2024-11-05 server could answer completion/complete with no capability; from 2025-03-26 “Servers that support completions MUST declare the completions capability”. A client gating on the capability will never ask a 2024-11-05 server unless it implements the era exception mcpx implements (see the disposition below). mcpx’s server serves from 2024-11-05, so the un-gated case is live on its server side too.

  • Disposition: Peer-settled — the client gates completion/complete on the declared completions capability, with the era exception implemented: a 2024-11-05 upstream (which had no capability to declare) is asked anyway, and its -32601 is read as the same absence a later server signals by not declaring (internal/mcpclient/request.go:52-65, covered by request_test.go). The server side is not moot: mcpx serves from 2024-11-05 (Oldest, internal/mcpserver/revisions.go:14).

C10 — “task required but not used” error code (2025-11-25, spec-internal).

  • Disposition: Undisposed — the actual behaviour is recorded, and it follows neither text: a non-task-augmented legacy call executes ordinarily, because maybeTask() returns nil before any task-support check when the call does not request a task (internal/mcpserver/tasks.go:237-240); the -32601 branch at :241-245 covers the opposite case, an augmented call to a tool whose support is “forbidden”. Open question: should the missing-required-task case refuse with -32601 (tool-level MUST) or -32600 (error list), or is ordinary execution the intended reading? See headline 13.
  • Tool-level: “If execution.taskSupport is "required" … Servers MUST return a -32601 (Method not found) error if a client does not attempt to do so.” https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation
  • Protocol errors: “receivers MAY return …: Non-task-augmented request when receiver requires task augmentation for that request type: -32600 (Invalid request)” …#protocol-errors (and the “Task augmentation required” example there uses -32600). For tools/call the MUST (-32601) should win; for other request types -32600.

C11 — cancelled task finality: MUST stay cancelled (2025-11-25) → may end non-cancelled (2026 extension).

  • Disposition: Undisposed — headline 13, first half: whether mcpx’s tasks implementation holds a cancelled task in cancelled (2025-11-25 core) or permits a later non-cancelled terminal status (extension) was not verified from internal/mcpserver/tasks.go in Phase 1.
  • 2025-11-25: “…MUST transition the task to cancelled status before sending the response.” “Once a task is cancelled, it MUST remain in cancelled status even if execution continues to completion or fails.” …#task-cancellation ; and the tasks/cancel response is the Task.
  • Extension: “Acknowledge cancellation requests with an empty result. Honor them when possible, but cancellation is cooperative — the task may still reach a non-cancelled terminal status.” docs/extensions/tasks/overview.mdx#for-mcp-servers ; lifecycle table: “cancelled | The operation was cancelled (not always honored).”
  • Also 2025-11-25 “Receivers MUST reject cancellation requests for tasks already in a terminal status … -32602” has no counterpart in the overview.

C12 — related-task on task-operation messages (2025-11-25, spec-internal).

  • Disposition: Recorded reading — the carve-out reading in the item text: the MUST covers messages of the underlying work and the tasks/result response; task-management messages follow their SHOULD NOTs.
  • “All requests, notifications, and responses related to a task MUST include the io.modelcontextprotocol/related-task key” …#associating-task-related-messages ; “All requests, responses, and notifications associated with a task MUST include…” …#related-task-metadata
  • vs “Requestors SHOULD NOT include io.modelcontextprotocol/related-task metadata in these requests” (tasks/get, /result, /cancel), “receivers SHOULD NOT include … in the result messages” (tasks/get, /list, /cancel), and “the notifications/tasks/status notification SHOULD NOT include the io.modelcontextprotocol/related-task metadata”. Read as: the MUST covers messages of the underlying work (e.g. the elicitation a task raises and its response), plus the tasks/result response (explicit MUST); task-management messages are carved out. The text does not say so.

C13 — extension opt-in wording (changelog vs extension page).

  • Disposition: Recorded reading — “unsolicited” is read as “without a task param”, not “without declaring the extension”: a task is never returned to a client that did not declare support (see the item text).
  • Changelog 2026-07-28 item 6: the extension “allows servers to return task handles unsolicited without per-request opt-in” https://modelcontextprotocol.io/specification/2026-07-28/changelog
  • Overview: “Never return a task to a client that did not declare support.” and “Clients opt in once via the extension capability … No per-tool warmup or per-request flag.” — but the capability itself is sent in per-request _meta (“Include the extension in its per-request capabilities”). “Unsolicited” means “without a task param”, not “without declaring the extension”. Model change vs 2025-11-25: requestor-driven (task param + tool taskSupport) → server-driven.
  • Also 2025-11-25 tasks/result, tasks/list and client-side tasks (task-augmented sampling/elicitation) do not exist in the extension; notifications/tasks/status → notifications/tasks via subscriptions/listen, yet core SubscriptionFilter has no field for it (presumably extension-defined; not verifiable without ext-tasks).

C14 — which results must carry cache hints (changelog vs caching page).

  • Disposition: Recorded reading — headline 12: the caching page and the schema govern over the changelog’s list; server/discover results are CacheableResults, and mcpx’s modern envelope emits ttlMs/cacheScope on them (headline 12).
  • Caching page includes server/discover in the MUST list; changelog item 5 lists “tools/list, prompts/list, resources/list, resources/read, and resources/templates/list” only. Page governs. Also the page says ttlMs absent “should only occur in older server versions” while schema makes ttlMs/cacheScope required — consistent (absence ⇒ legacy server). And CacheableResult doc: “how long … the client MAY cache this response before re-fetching” vs page “how long … the client MAY consider the result fresh” — wording only.

C15 — 2026-07-28 “stream stays open … until the client cancels it” vs server teardown. Subscriptions intro: “the stream stays open and delivers notifications until the client cancels it”, then lists server teardown and transport close as other endings. Descriptive inconsistency only.

  • Disposition: Recorded reading — descriptive inconsistency only, as the item states; the enumerated endings (client cancel, server teardown, transport close) govern over the intro’s shorthand.

D. Tools, resources, prompts, discover

C1 — tool input-validation errors: protocol error → execution error (cross-revision).

  • Disposition: Undisposed — headline 5: the client choke keeps -32602 (internal/mcpclient/schemacheck.go, internal/daemon/callerr.go); whether the serve seam era-splits isError for >=2025-11-25 hosts is the open question named there. 2024-11-05…2025-06-18: “Protocol Errors: Standard JSON-RPC errors for issues like: … Invalid arguments” and execution errors are “Invalid input data” — https://modelcontextprotocol.io/specification/2025-06-18/server/tools#error-handling. 2025-11-25+: “Tool Execution Errors … Input validation errors (e.g., date in wrong format, value out of range)”; protocol errors are only “Malformed requests (requests that fail to satisfy CallToolRequest schema)” — https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling. Changelog: “Clarify that input validation errors should be returned as Tool Execution Errors rather than Protocol Errors” (SEP-1303). A server emitting -32602 for a bad date is following 2025-06-18 and deviating from 2025-11-25. isError:true is legal in both, so the conservative choice is isError.

C2 — resource-not-found code: -32002 SHOULD → -32602 MUST (cross-revision).

  • Disposition: Peer-settled — headline 6: the code is picked per request revision in code (legacy -32002, codeResourceNotFoundLegacy, internal/mcpserver/server.go; 2026 -32602). ≤2025-11-25: “Resource not found: -32002” (SHOULD) — https://modelcontextprotocol.io/specification/2025-11-25/server/resources#error-handling. 2026-07-28: “servers MUST return a JSON-RPC error with code -32602”; “clients SHOULD also accept -32002” — https://modelcontextprotocol.io/specification/2026-07-28/server/resources#error-handling. Note 2026 also forbids new use of -32000..-32019 codes except grandfathered (basic/index#error-codes), and -32002 is in that range. A dual-era server must pick the code per request revision.

C3 — resources list_changed delivery: 2026 resources page vs 2026 subscriptions rules (spec-internal).

  • Disposition: Recorded reading — follow the schema, as the item states: resources/list_changed is sent only on a listen stream whose filter requested it; the resources page’s unqualified SHOULD does not govern. 2026 resources: “servers that declared the listChanged capability SHOULD send a notification” (no stream qualifier) — https://modelcontextprotocol.io/specification/2026-07-28/server/resources#list-changed-notification. 2026 tools/prompts say “to clients that have opened a subscriptions/listen stream with toolsListChanged: true”, and SubscriptionFilter says “the server MUST NOT send notification types the client has not explicitly requested here” — https://modelcontextprotocol.io/specification/2026-07-28/schema#subscriptionfilter. Read literally, the resources page permits an unsolicited push that the schema forbids; the resources page was not updated. Follow the schema: only on a listen stream with resourcesListChanged: true.

C4 — resources.subscribe capability meaning (cross-revision).

  • Disposition: Undisposed — open question: is the legacy resources/subscribe RPC answered only on legacy connections, and is the same capability flag read as the listen-filter meaning on 2026-07-28? No site verified in Phase 1. Legacy: client may call resources/subscribe (https://modelcontextprotocol.io/specification/2025-11-25/server/resources#capabilities). 2026: “whether the server supports resource-specific update notifications for resources requested through subscriptions/listen using the resourceSubscriptions filter” (https://modelcontextprotocol.io/specification/2026-07-28/server/resources#capabilities). resources/subscribe/unsubscribe are removed (“Replaces the former resources/subscribe RPC”, schema#subscriptionfilter; changelog item 4). Same flag, different method behind it.

C5 — structuredContent / outputSchema shape (cross-revision; prose vs schema in 2025-06-18/11-25).

  • Disposition: Undisposed — headline 7: downgrade() (internal/mcpserver/revisions.go:237) deletes structuredContent only for pre-2025-06-18; what happens to a non-object value headed for a 2025-06-18..2025-11-25 host is the open question named there. 2025-06-18/2025-11-25 schema: structuredContent?: { [key: string]: unknown }; outputSchema “Currently restricted to type: "object" at the root level.” — https://modelcontextprotocol.io/specification/2025-11-25/schema#calltoolresult. 2026-07-28: “This can be any JSON value (object, array, string, number, boolean, or null)”; outputSchema “can be any valid JSON Schema 2020-12” — https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content. A 2026 upstream’s array result is illegal to send to a 2025-11-25 client verbatim. (The 2025-06-18/11-25 prose says “a JSON object”; consistent with its schema.)

C6 — list/changed delivery and per-connection variance (cross-revision).

  • Disposition: Undisposed — open question: does mcpx’s server deliver list_changed per era (session-wide legacy vs opted-in listen streams in 2026), and can its served lists vary per connection against the 2026 MUST NOT? No site verified in Phase 1. Legacy list_changed is sent on the session to any client; 2026 only to opted-in listen streams (tools/prompts pages). 2026 adds “MUST NOT vary per-connection” for tools/prompts/resources lists — https://modelcontextprotocol.io/specification/2026-07-28/server/tools#capabilities; legacy imposes nothing, so session-scoped tool sets were legal before.

C7 — where capabilities are declared (page vs page, 2026).

  • Disposition: Undisposed — open question: does mcpx’s DiscoverResult declare the prompts capability in the result itself, as the 2026 prompts page requires, and the tools/resources capabilities observably at all? No site verified in Phase 1. 2026 prompts: “MUST declare the prompts capability in their DiscoverResult” — https://modelcontextprotocol.io/specification/2026-07-28/server/prompts#capabilities. 2026 tools/resources: “MUST declare the tools capability” with no location — https://modelcontextprotocol.io/specification/2026-07-28/server/tools#capabilities. Meanwhile discover prose makes calling discover optional (“a client may invoke any RPC inline”). So a client that never calls discover never sees server capabilities, yet tools/resources/prompts listChanged/subscribe semantics depend on them. Not a contradiction, but the capability is only observable via discover (or the listen ack’s echoed filter).

C8 — embedded resource “MUST include … MIME type” vs schema (prose vs schema, all revs).

  • Disposition: Recorded reading — as the item states: senders include the MIME type; receivers accept its absence, the schema’s optional field governing interop. Prompts prose: “MUST include: A valid resource URI; The appropriate MIME type; …” — https://modelcontextprotocol.io/specification/2026-07-28/server/prompts#embedded-resources. Schema ResourceContents.mimeType? is optional in every revision. Senders should include it; receivers must accept its absence.

C9 — resource_link in prompts (prose vs schema, 2025-06-18/2025-11-25).

  • Disposition: Recorded reading — the schema governs: ResourceLink is a legal PromptMessage content block from 2025-06-18, whether or not that era’s page documents it (verified in the item text). PromptMessage.content: ContentBlock includes ResourceLink in the 2025-06-18 and 2025-11-25 schemas, but only the 2026 prompts page documents “Prompt messages MAY include links to Resources” — https://modelcontextprotocol.io/specification/2026-07-28/server/prompts#resource-links. (Verified: 2025-06-18 schema PromptMessage.content: ContentBlock, and ContentBlock includes ResourceLink.)

C10 — discover call: MAY vs SHOULD (page-internal, conditional).

  • Disposition: Recorded reading — MAY in general, SHOULD for dual-era clients, and mcpx follows the SHOULD by default: an uncached connection under the default preference (PreferModern, internal/mcpclient/client.go:321-325) probes server/discover first (internal/mcpclient/probe.go:296). initialize goes first only under a legacy preference or a cached legacy era. “Calling server/discover is optional for clients” / schema “Clients MAY call it” vs “A client that supports both modern… and legacy… servers SHOULD send server/discover first” — https://modelcontextprotocol.io/specification/2026-07-28/server/discover#when-to-call. Reconciles as: MAY in general, SHOULD for dual-era clients on stdio. mcpx (dual-era client) probes initialize first by design.

C11 — CacheableResult coverage: changelog vs schema.

  • Disposition: Recorded reading — headline 12: the schema governs over the changelog; server/discover also requires ttlMs/cacheScope, and mcpx’s modern envelope emits both on the discover result. Changelog: “Require ttlMs and cacheScope fields on results returned by tools/list, prompts/list, resources/list, resources/read, and resources/templates/list” — https://modelcontextprotocol.io/specification/2026-07-28/changelog. Schema: DiscoverResult extends CacheableResult, and discover.mdx “This operation supports caching.” Changelog omits discover (and prompts/get). Schema governs: discover also requires both fields.

C12 — serverInfo placement (cross-revision) and required-ness.

  • Disposition: Recorded reading — same disposition as section A item C17: mcpx follows the 2026 placement on the modern path and the required top-level field on the legacy path. InitializeResult.serverInfo is a required top-level field in legacy; in 2026 it moves to _meta['io.modelcontextprotocol/serverInfo'] and is optional SHOULD (https://modelcontextprotocol.io/specification/2026-07-28/server/discover#discoverresult). Same for protocolVersion (single) → supportedVersions (list).

C13 — tool name guidance vs aggregator reality.

  • Disposition: Undisposed — open question: when mcpx prefixes upstream tool names for disambiguation, what happens to names that would exceed 128 chars or leave the charset? No site verified in Phase 1. “Tool names SHOULD be unique within a server” + charset [A-Za-z0-9_.-] + ≤128 chars (2025-11-25+) vs the 2026 note that proxies “SHOULD implement a disambiguation strategy such as prefixing tool names with a server identifier” — prefixing makes the 128 limit and charset binding on the proxy’s separator and on upstream names that are already long or non-conforming. Not contradictory; a squeeze.

C14 — isError vs “tool not found” (schema vs mcpx, not spec-internal). Recorded here because it is easy to misread: the CallToolResult comment keeps “errors in finding the tool … should be reported as an MCP error response” in every revision; only argument validation moved (C1).

  • Disposition: Recorded reading — as the item states: errors in finding the tool are MCP error responses in every revision; only argument validation moved (section D item C1).

E. Roots, sampling, elicitation

  1. Carriage of all three requests. 2024-11-05–2025-11-25: server sends a JSON-RPC request
  • Disposition: Peer-settled — mcpx bridges the eras by translation: wire requests on legacy connections, inputRequests/inputResponses entries on modern ones (internal/mcpserver/ask.go, internal/mcpserver/conn.go). ("To request information from a user, servers send an \elicitation/create` request.“, https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#elicitation-requests). 2026-07-28: “The previous pattern of server-initiated requests is no longer supported. This is a breaking change.” (https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr).ClientResultin the 2026 schema is EmptyResultonly — there is no client→server response message type at all. A proxy bridging eras must translate wire request ⇄inputRequestsentry, and wire response ⇄inputResponses` entry.
  1. Capability declaration. “during initialization” (≤2025-11-25) → “in _meta.io.modelcontextprotocol/clientCapabilities
  • Disposition: Peer-settled — era-keyed in code: declared at initialize on legacy connections and in per-request _meta client capabilities on modern ones, for roots, sampling and elicitation alike. on each request“ (2026-07-28), for roots, sampling and elicitation. Same URLs as rows *-declare-capability-*.
  1. roots.listChanged and notifications/roots/list_changed. ≤2025-11-25: “When roots change, clients that support
  • Disposition: Peer-settled — the notification exists only in the legacy era; methods a revision removed are answered -32601 on the modern path (internal/mcpserver/revisions.go:139; internal/mcpserver/server.go:916). listChanged MUST send a notification“ (https://modelcontextprotocol.io/specification/2025-11-25/client/roots#root-list-changes). 2026-07-28: capability is roots?: {}; changelog item 5: “Remove ping, logging/setLevel, and notifications/roots/list_changed” (https://modelcontextprotocol.io/specification/2026-07-28/changelog).
  1. Roots semantics. ≤2025-11-25: “Roots define the boundaries of where servers can operate within the filesystem”.
  • Disposition: Recorded reading — for modern peers mcpx follows the 2026 reading (roots are informational guidance, not an access-control boundary); the legacy boundary wording governs legacy peers only. 2026-07-28: “They are informational guidance rather than an access-control mechanism. The protocol does not enforce that servers stay within roots.” (https://modelcontextprotocol.io/specification/2026-07-28/client/roots).
  1. Roots, Sampling deprecated (SEP-2577) in 2026-07-28: “New implementations SHOULD NOT adopt it”
  • Disposition: Recorded reading — deprecation is read as “should not adopt anew”: mcpx continues to implement roots and sampling because its legacy peers require them; it does not treat the features as removed. (https://modelcontextprotocol.io/specification/2026-07-28/deprecated). Elicitation is not deprecated.
  1. Sampling error path. ≤2025-11-25: “Clients SHOULD return errors for common failure cases” with -1 for a
  • Disposition: Undisposed — open question: under MRTR there is no error channel (see the spec-internal item 6 below); how does mcpx’s client signal a declined or failed sampling request to a 2026-07-28 server — absence of a retry only? No site verified in Phase 1. rejected request (https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#error-handling). 2026-07-28: “If an error occurs or the user declines the sampling request, the client does not need to replay the initial call with an error message” (https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#error-handling). Roots has the same change (-32601/-32603 → no error path).
  1. Tools in sampling exist only from 2025-11-25 (tools, toolChoice, tool_use/tool_result content, content
  • Disposition: Undisposed — open question: does mcpx shape sampling messages per peer era (single-block content for pre-2025-11-25 peers, arrays and tool content after), as it shapes structuredContent in downgrade() (internal/mcpserver/revisions.go)? The sampling path was not verified in Phase 1. arrays, stopReason: "toolUse"). 2024-11-05 SamplingMessage.content is TextContent | ImageContent (single block); 2025-03-26 adds audio; arrays only from 2025-11-25. A 2025-11-25+ result with array content is unparseable by an older peer.
  1. includeContext values. Plain in ≤2025-06-18; “soft-deprecated. Servers SHOULD avoid using these values …”
  • Disposition: Recorded reading — as the item states, the two texts are consistent in substance; the soft-deprecation date (2025-11-25) is the operative one. (https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#capabilities); “deprecated under the feature lifecycle policy (SEP-2596)” in 2026-07-28. Schema 2026 @deprecated says “as of protocol version 2025-11-25” while the 2026 changelog says “Reclassify … (soft-deprecated since protocol version 2025-11-25) as Deprecated” — consistent in substance, differing in which date is “deprecated”.
  1. Elicitation sensitive information. 2025-06-18: “Servers MUST NOT use elicitation to request sensitive
  • Disposition: Undisposed — open question: does mcpx gate elicitation modes per era — form mode barred from sensitive information from 2025-11-25, URL mode required for it, the 2025-06-18 all-modes ban before that? The elicitation path (internal/mcpserver/ask.go) was not audited for this in Phase 1. information.“ (all modes; https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#user-interaction-model). 2025-11-25+: only form mode is forbidden, and URL mode is required for it.
  1. Client UI obligations promoted SHOULD → MUST in 2025-11-25: which-server indication, review/modify (form),
  • Disposition: Undisposed — open question (host-side): mcpx’s client surfaces elicitation to its host UI; whether the 2025-11-25 MUSTs (which-server indication, review/modify, decline/cancel) are all met by that surface was not audited in Phase 1. decline/cancel options. 2025-06-18 “Applications SHOULD: …” vs 2025-11-25 “MCP clients MUST: …” (https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#user-interaction-model).
  1. Elicitation request shape. 2025-06-18 params: {message, requestedSchema} — no mode, no default, enum is
  • Disposition: Undisposed — open question: are elicitation params shaped per peer era (mode/elicitationId/url mode withheld from a 2025-06-18 peer, elicitationId withheld from 2026-07-28)? No site verified in Phase 1. {enum, enumNames}. 2025-11-25 adds mode, url mode, elicitationId, defaults, oneOf/array enums, LegacyTitledEnumSchema. 2026-07-28 removes elicitationId. Sending mode/elicitationId to a 2025-06-18 peer is sending fields its schema lacks; sending url mode to it is sending a request it cannot represent.
  1. elicitationId, notifications/elicitation/complete, URLElicitationRequiredError (-32042) exist only in
  • Disposition: Peer-settled — the trio exists in the 2025-11-25 era only, and 2026-07-28 forbids emitting -32042; mcpx’s client handles and propagates -32042 on that era’s path (internal/mcpclient/client.go:1549-1586), while the server-side return gap is recorded at the spec-internal item 2 below. 2025-11-25. 2026-07-28 changelog item 11 removes the first two; basic/index: “Implementations of this protocol version MUST NOT emit these codes: … -32042” (https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes).
  1. Elicitation statefulness. 2025-11-25: “Most practical uses of elicitation require that the server maintain state”
  • Disposition: Undisposed — open question: does mcpx’s URL-mode elicitation keep server state keyed by anything other than a session ID alone (2025-11-25 rule), and does it rely on no state at all in 2026 form mode? Not audited in Phase 1.
    • “State MUST NOT be associated with session IDs alone” (https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#statefulness). 2026-07-28: “Elicitations do not require that the server maintain state … However, if state is stored …” and the session-ID rule is gone (sessions no longer exist) (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#statefulness).
  1. Elicitation error handling. 2025-11-25 prescribes -32042 (server) and -32602 for an undeclared mode (client).
  • Disposition: Undisposed — open question: which error surface does mcpx use per era — -32042 / -32602 in 2025-11-25 (but see the spec-internal item 2 below) vs decline/cancel handling with no codes in 2026-07-28? Cross-reference headline 5’s serve-seam question. 2026-07-28 replaces both with “Servers SHOULD NOT assume … and MUST handle cases where the user declines or cancels …” (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#error-handling).
  1. Client rate limiting for elicitation: SHOULD in 2025-06-18 and 2025-11-25, dropped in 2026-07-28 (sampling keeps it).
  • Disposition: Undisposed — open question: does mcpx’s client rate-limit elicitation requests at all, and if so is the limit era-scoped now that 2026-07-28 drops the SHOULD? Not audited in Phase 1.
  1. New 2026-only sampling MUSTs: “Sampling messages MUST contain a role … and a content field”, “The client
  • Disposition: Undisposed — open question: does mcpx’s client enforce the 2026 sampling MUSTs (role and content present, maxTokens respected, no retention between requests) on MRTR-carried sampling? Not audited in Phase 1. MUST respect the maxTokens parameter“, “SHOULD NOT be retained between separate requests” (https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#messages, https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#sampling-parameters).
  1. Task-augmented sampling/elicitation (params.task, tasks.requests.sampling.createMessage,
  • Disposition: Undisposed — headline 13 area: task augmentation exists only in 2025-11-25 core; whether mcpx strips or rejects a task param headed for a 2026-07-28 peer was not verified in Phase 1. tasks.requests.elicitation.create) exist in 2025-11-25 core (https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks). The 2026-07-28 CreateMessageRequestParams and ElicitRequest*Params have no task field, and 2026 ClientCapabilities has no tasks.

Spec-internal contradictions

  1. mode “MUST include” yet optional (2025-11-25, 2026-07-28): “All elicitation requests MUST include the following
  • Disposition: Recorded reading — as the item states: only message is unconditionally required; mode defaults to "form" and may be omitted for form mode. parameters: … mode … Optional for form mode (defaults to \"form\" if omitted).“ followed by “servers MAY omit the mode field” (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#elicitation-requests). Resolution by reading: only message is unconditionally required.
  1. -32042: MAY vs MUST (2025-11-25). “the server MAY return a URLElicitationRequiredError”
  • Disposition: Recorded reading (reading chosen; implementation gap recorded) — mcpx’s reading is the MUST: a request that cannot be processed until an elicitation completes should return -32042. The server does not yet do it: a URL question the legacy client cannot answer stays with the broker while the server waits (internal/mcpserver/ask.go:192-207), and the gap is recorded in the conformance registry (elicitation-server-must-return-32042, internal/conformance/coverage_clientfeatures.go, #214). The client side does handle receiving -32042 (internal/mcpclient/client.go:1549-1586). (https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-error) vs “Servers MUST return standard JSON-RPC errors for common failure cases: - When a request cannot be processed until an elicitation is completed: -32042” (https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#error-handling). Also the schema marks URL_ELICITATION_REQUIRED and URLElicitationRequiredError @internal, and the 2025-11-25 changelog links #url-elicitation-requests, an anchor that does not exist (heading is “URL Mode Elicitation Requests”).
  1. Roots: guidance vs access control (2026-07-28). “They are informational guidance rather than an access-control
  • Disposition: Recorded reading — the roots page’s main text governs (informational guidance) over the security-considerations boilerplate; see the cross-revision item 4 above. mechanism“ (https://modelcontextprotocol.io/specification/2026-07-28/client/roots) vs “Clients MUST: … Implement proper access controls” and servers “SHOULD … Validate all paths against provided roots” (https://modelcontextprotocol.io/specification/2026-07-28/client/roots#security-considerations).
  1. Roots: change monitoring without a change signal (2026-07-28). “Clients SHOULD: … Monitor for root changes”
  • Disposition: Undisposed — open question: in 2026-07-28 there is no signal to monitor with; does mcpx’s client re-list roots per request instead of caching them, and if it caches, what invalidates the cache? Not audited in Phase 1. (https://modelcontextprotocol.io/specification/2026-07-28/client/roots#implementation-guidelines) survives, but there is no longer any way to tell the server; and “Cache root information appropriately” has no invalidation signal.
  1. Deprecated but MUST declare (2026-07-28): roots and sampling pages say new implementations “SHOULD NOT adopt it”
  • Disposition: Recorded reading — as the item states, the texts are consistent read as “if you implement it anyway”: support entails the per-request declaration MUST; non-adoption is the SHOULD NOT’s subject. while still “Clients that support roots MUST declare … on each request” — consistent only if read as “if you implement it anyway”.
  1. Tools-without-capability: client MUST error, but 2026 has no error channel. Schema 2026: “The client MUST return an
  • Disposition: Undisposed — open question (same gap as the cross-revision item 6 above): the only expressible client “error” under MRTR is not retrying, or a client-side failure; which one mcpx’s client produces for an undeclared sampling.tools field was not verified in Phase 1. error if this field is provided but ClientCapabilities.sampling.tools is not declared.“ (https://modelcontextprotocol.io/specification/2026-07-28/schema#createmessagerequestparams). In 2026 the request arrives inside InputRequiredResult.inputRequests; InputResponse is CreateMessageResult | ListRootsResult | ElicitResult — no error member — and the sampling page says “the client does not need to replay the initial call with an error message”. The only expressible “error” is not retrying, or a client-side failure.
  1. Elicitation “client fails to process” in 2026. “MUST handle cases where … the client fails to process the
  • Disposition: Undisposed — open question: under MRTR the server sees only absence; does mcpx’s server re-request missing elicitation input (the mrtr SHOULD) or fail the original request? Not audited in Phase 1. request“ (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#error-handling) — under MRTR the server cannot observe that; it sees only absence of a retry, or a retry missing that key (mrtr: “SHOULD respond with a new InputRequiredResult requesting the missing information again”).
  1. Missing capability: error vs. omit (2026-07-28). basic/index: “the server MUST return a
  • Disposition: Undisposed — open question: when mcpx’s server requires elicitation a client has not declared, does it return -32021 (MissingRequiredClientCapabilityError), and when it can degrade, does it degrade silently? Not audited in Phase 1. MissingRequiredClientCapabilityError (-32021)“ (https://modelcontextprotocol.io/specification/2026-07-28/basic/index#meta) vs mrtr: “Servers MUST NOT send an inputRequests that the client has not declared support for” and elicitation: “Servers SHOULD NOT assume that elicitation requests will always succeed”. Together they force: a server that requires elicitation errors -32021; one that can degrade must degrade silently. Neither says which a server that could use elicitation but has a fallback must pick.
  1. Elicitation page vs mrtr on where elicitation may occur. “Servers MAY request information from a user during the
  • Disposition: Undisposed — open question: which client requests may carry an InputRequiredResult from mcpx’s server — any (elicitation page) or only prompts/get, resources/read, tools/call (mrtr)? Check the ask path in internal/mcpserver/ask.go. processing of a client request“ (any request) vs mrtr “Servers MUST NOT send InputRequiredResult responses on any other client requests” than prompts/get, resources/read, tools/call (https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#supported-requests).
  1. _meta preservation addressed to the wrong side. Schema ToolUseContent._meta / ToolResultContent._meta:
  • Disposition: Undisposed — open question: mcpx’s server constructs subsequent sampling requests; does it preserve ToolUseContent._meta / ToolResultContent._meta when doing so, as the mis-addressed SHOULD intends? Not audited in Phase 1. “Clients SHOULD preserve this field when including tool uses in subsequent sampling requests” (https://modelcontextprotocol.io/specification/2026-07-28/schema#toolusecontent) — subsequent sampling requests are constructed by the server.
  1. maxTokens: MUST respect vs MAY sample fewer (2026-07-28). Compatible (a ceiling), but “respect” is undefined
  • Disposition: Recorded reading — as the item states, the texts are compatible: maxTokens is a ceiling; sampling fewer is respecting it. beyond not exceeding it (https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#sampling-parameters vs schema CreateMessageRequestParams.maxTokens).
  1. 2025-11-25 StringSchema example uses pattern ("pattern": "^[A-Za-z]+$",
  • Disposition: Recorded reading — the type governs over the example: pattern is not a 2025-11-25 StringSchema keyword, whatever the example shows; the 2026 example’s removal confirms the reading. https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#requested-schema), a keyword the 2025-11-25 StringSchema type does not define. Removed from the 2026 example.
  1. Unconditional user binding (2025-11-25, 2026-07-28): “Servers MUST bind elicitation requests to the
  • Disposition: Undisposed — open question: how does mcpx bind an elicitation request to a user identity on stdio or without authorization, where no identity exists? Not audited in Phase 1. client and user identity“ while there is no user identity on stdio / without authorization; “when possible” appears only in the Statefulness bullet (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations).
  1. Sampling “Result Fields” vs tool-result constraint (2026-07-28): result content may be “multiple tool uses or
  • Disposition: Recorded reading — as the item states: the user-message ONLY-tool-results rule is not applied to result content; a proxy must not apply it there. mixed content“ (assistant side) — not a contradiction with “user message … MUST contain ONLY tool results”, but a proxy must not apply the user-message rule to results.
  1. URL-mode requestState and “stateless” claim (2026-07-28): Statefulness says elicitation does not require server
  • Disposition: Undisposed — open question (same area as the cross-revision item 13 above): token custody makes the OAuth server stateful regardless of elicitation’s statelessness; whether mcpx’s URL-mode flow stores requestState server-side was not audited in Phase 1. state, while the OAuth section still requires “The MCP server is responsible for tokens … (in other words, the MCP server must be stateful)” (https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#understanding-the-distinction).

What the specification asks of a server

created:      2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T16:00:00-05:00
increment:    3
status:       standard
tags:         area:protocol, area:spec
description:  per revision, what an MCP server MUST, SHOULD and MAY do,
              including the obligations the prose implies but never states.

This document is about the specification, not about mcpx. It exists because the spec’s MUSTs are scattered: across the lifecycle, transport, versioning and utility pages, and across the doc comments in schema.ts, which carry several rules the prose never repeats. A server written from the feature pages alone misses most of them. What mcpx does is summarised in a line where it matters and documented in ../protocol.md; the per-requirement status, each with its test, is conformance-matrix.md. The feature-by-revision table (which method, field, code and header exists where) is ../compare/matrix-revisions.md, the character of each revision is ../compare/what-a-server-should-be.md, and every place two sources disagree is ../compare/conflicts.md; none of that is repeated here. The client side is client-obligations.md.

This document is the distilled form. The authority for what mcpx actually does is the code — internal/mcpserver/revisions.go holds the floors, ceilings and removals as data, and internal/mcpspec validates real traffic against the official schemas for all five revisions. A matrix in a document and a matrix in code agree on the day they are written and never again, so where they differ, the code is right and this file is stale.

Revisions are written by date. Legacy means 2024-11-05 through 2025-11-25, which share an initialize handshake. Modern means 2026-07-28, which has no handshake.

URLs abbreviate https://modelcontextprotocol.io/specification/ as spec/.


1. Lifecycle

Legacy (2024-11-05 .. 2025-11-25)

The session begins with initialize (spec/2025-11-25/basic/lifecycle#initialization).

  • A server MUST respond to initialize with its capabilities and serverInfo.
  • If it supports the requested protocolVersion it MUST echo that version. Otherwise it MUST answer with another version it supports, which SHOULD be its latest (#version-negotiation). The answer is a result, not an error, and the client decides whether it can continue. Answering with an error takes that decision away from the client. The modern error code -32022 did not exist in any legacy revision, so a legacy client has no rule for recognising it.
  • Before notifications/initialized, a server SHOULD NOT send anything except pings and logging.
  • A server MUST NOT send a request or notification outside the capabilities it declared. Nor may it use a client capability the client did not declare. Sampling, roots and elicitation are all gated on the client’s declaration.
  • Shutdown is transport-level. On stdio the client closes stdin, and a server MAY also exit on its own. The legacy HTTP transports have no shutdown message.

The negotiated version, the client’s capabilities, the log level and the resources/subscribe set are session state. §3.4 covers where a dual-era server keeps it.

Modern (2026-07-28)

There is no handshake. Every request carries _meta["io.modelcontextprotocol/protocolVersion"] and _meta["io.modelcontextprotocol/clientCapabilities"], and both are REQUIRED (spec/2026-07-28/basic/versioning).

  • A server MUST implement server/discover (spec/2026-07-28/server/discover). The DiscoverResult is {supportedVersions, capabilities, instructions?, _meta: {"io.modelcontextprotocol/serverInfo": …}, ttlMs, cacheScope, resultType}. The field is supportedVersions, not protocolVersions, and serverInfo lives in _meta, not at the top level. Discovery is a cacheable result, so ttlMs and cacheScope are required here too, even though the changelog’s list omits server/discover.
  • A request whose version the server does not support MUST be rejected with -32022, and the supported versions go in data.supported.
  • A request that omits the required _meta fields MUST be rejected with -32602.
  • A server MUST NOT infer client capabilities from earlier requests (schema RequestMetaObject). The capabilities on this request are the only ones that exist for it.
  • Every result MUST carry resultType. A client treats an absent resultType as "complete".
  • A server SHOULD put its serverInfo in every result’s _meta.
  • A server MUST NOT send requests to the client at all. Questions for the client travel as MRTR inputRequests inside a result (§4.4).

Statelessness is a rule, not a transport detail. State that spans requests “MUST be referenced by an explicit identifier the client passes on each request”, and “an open connection, such as a STDIO process, is not a conversation or session”. A server “SHOULD NOT require that a client reuse the same connection or process”. Three consequences follow, none of them spelled out:

  • A requestState, a task id or a subscription cannot be validated by comparing it with the connection it arrived on. If a server needs to bind a handle to a caller, the binding has to be inside the handle (signed claims: principal, expiry, originating method and parameters) or come from the authorization identity. mcpx binds requestState to the method, a digest of the parameters and an expiry, and to no connection (messages.md).
  • A retry may reach a different process or instance. Any integrity key a handle depends on has to be shared by every process that might receive the retry. With a per-process key the retry still fails verification, which is safe, but the work is lost.
  • The server’s capabilities are advertised once, by server/discover, and cached for ttlMs. The client’s capabilities are restated on every request and may change from one request to the next. There is no symmetric restatement: a client acts on server capabilities that may be up to ttlMs stale, and a server acts on client capabilities that are exactly current.

What “stateless” does and does not forbid, item by item, and where a proxy legitimately keeps state, is ../compare/stateless.md.


2. Versioning and eras

The 2026 versioning page makes era “a property of the server, not of an individual request”. A client probes once, then caches the result for the lifetime of the process (stdio) or origin (HTTP), and MAY persist it across restarts of the same configuration.

For a pool of processes of one configuration, that sentence has a consequence the spec does not draw. Every process in the pool must answer the same era. If two members disagree, the binary changed underneath the pool, and the client’s cached answer is wrong for part of it. Since the cache key is the configuration rather than the process, a server must not be dual-era on some launches and modern-only on others. Each freshly spawned stdio process still needs its own initialize if it is legacy, because a new process is a new session, even though the era answer is inherited.

A dual-era server answers both initialize and _meta-versioned requests. It faces three kinds of inbound request, and the spec defines two of them:

inboundspec sayswhat the server has to do
_meta carries a protocol versionmodern semantics, statelessignore any legacy session state; use this request’s capabilities
initialize, or a request on a session that was initialisedlegacy semantics, scoped to the process (stdio) or session (HTTP)keep the version and capabilities from initialize
neithermodern: MUST reject -32602; legacy: “not the first interaction”unspecified. mcpx serves it as 2025-03-26, a choice rather than a rule (../compare/stateless.md §4)

A modern-only server that receives initialize SHOULD name its supported versions in the error. A dual-era server has to decide per request whether a Mcp-Session-Id header is legacy state or noise. The reliable signal is the body: a _meta protocol version means modern.


3. Transports

3.1 stdio (all revisions)

  • Messages are newline-delimited JSON-RPC and MUST NOT contain embedded newlines.
  • The server MUST NOT write anything to stdout that is not a valid MCP message. stderr is for logs, and in 2025-11-25 and later it MAY carry any output, which clients must not treat as an error.
  • Modern: stdio is still stateless. initialize makes the whole pipe legacy for its lifetime, because one process serves one client. In 2026 a server sends notifications/cancelled only to end a subscriptions/listen stream, and only on stdio (§4.2).
  • The 2026 page recommends newline-delimited JSON for custom transports over Unix sockets and TCP as well. Only process launch, stderr, EOF shutdown and restart are specific to stdio.

3.2 HTTP+SSE (2024-11-05; deprecated)

The server exposes an SSE endpoint and a POST endpoint.

  • On connect it MUST send an endpoint event naming the POST URL.
  • Every server message travels as an SSE message event.

From 2025-03-26 on, a server that still offers this transport does so only for older clients, and new implementations SHOULD NOT adopt it.

3.3 Streamable HTTP (2025-03-26 .. 2026-07-28)

One endpoint answers POST and, in the legacy revisions, GET.

  • Origin. Every revision: a server MUST validate Origin to prevent DNS rebinding, and from 2025-11-25 an invalid origin gets 403. On a loopback, unauthenticated server this is the only defence the transport offers against any web page the user visits. Binding to loopback alone does not stop DNS rebinding. (mcpx checks it on /mcp and on the daemon’s /v1 routes beside it: transport.md, internal/daemon/origin.go.)
  • Local binding. A local server SHOULD bind to 127.0.0.1 and SHOULD authenticate.
  • POST responses. A POST carrying only responses or notifications gets 202. A POST carrying a request gets either application/json or an SSE stream, and the client must accept both.
  • Batches. 2025-03-26 alone requires a server to accept JSON-RPC batches. 2025-06-18 removed them.
  • Sessions (legacy). The server MAY assign Mcp-Session-Id in the initialize response. After that it SHOULD reject requests without the id with 400, and it MUST answer 404 for an id it has terminated. The 404 is how a client learns it has to re-initialise.
  • Protocol version header (2025-06-18 and later). The server MUST reject an unsupported MCP-Protocol-Version with 400. With no header, it assumes 2025-03-26 (SHOULD, and MAY in 2026).
  • Resumability and redelivery (legacy). Event ids and Last-Event-ID resumption are optional. Where they are offered, a message MUST NOT be broadcast on more than one stream.
  • Modern (2026-07-28).
    • No sessions, no GET stream, no resumption. A modern-only server should neither mint nor echo Mcp-Session-Id.
    • Mcp-Method and Mcp-Name headers mirror the body, and so do Mcp-Param-* headers for tool parameters annotated x-mcp-header. The server that processes the body MUST validate them and reject a mismatch with -32020 (HTTP 400).
    • An unknown method gets 404 with -32601.
    • Errors that the schema assigns HTTP statuses MUST be JSON-RPC error bodies. A bare {"error": …} or a 200-status error makes a dual-era client misclassify the server as legacy and fall back to initialize. Era detection is only as reliable as the server’s error bodies.
    • A client disconnect MUST be treated as cancellation. In every legacy revision it SHOULD NOT be (§4.2).

3.4 Where a dual-era server keeps legacy state

The 2026 versioning page scopes an initialize “to the stdio process (stdio) or the session (HTTP)”. So legacy state is per process on stdio and per session on HTTP:

  • On stdio, there is no smaller unit. A second initialize on the same process had no defined meaning in any legacy revision.
  • On HTTP, each Mcp-Session-Id carries its own negotiated version, client capabilities, log level, subscriptions and pending server-to-client requests. Modern requests on the same endpoint must not see any of it.
  • A legacy HTTP client that ignores the session header has no state beyond one POST. The server can ask it a question only on that POST’s own SSE stream, and the answer arrives on a separate POST that cannot be routed back without a session.

3.5 Authorization (2025-03-26 and later, HTTP only)

Authorization is an HTTP concern. A stdio server gets its credentials from the environment and SHOULD NOT use the OAuth flow.

An HTTP server that implements authorization:

  • MUST publish Protected Resource Metadata. From 2025-06-18 it answers 401 with WWW-Authenticate naming that metadata.
  • MUST validate that tokens were issued for it (audience).
  • MUST NOT pass a client’s token through to an upstream API. This is the confused-deputy rule.
  • A proxy that uses a static client id upstream owes per-client consent (2025-06-18 and later).

A server that does no authorization has none of these obligations. It also has no principal to bind requestState, tasks or elicitations to, which matters in §4.4 and §5.


4. Messages and utilities

4.1 JSON-RPC and errors

  • A request id MUST NOT be null (all revisions).
  • A request id MUST NOT be reused within a session (legacy). In 2026 reuse is legal, but a server that also speaks legacy, or that correlates subscriptions by id, should never reuse one.
  • A parse error omits id from 2025-11-25 on. The 2025-03-26 and 2025-06-18 schemas have no conformant form for it.
  • Error codes are era-scoped:
    • -32002 (resource not found) is legacy; 2026 uses -32602.
    • -32042 (URL elicitation required) exists only in 2025-11-25.
    • -32020, -32021 and -32022 exist only in 2026.
    • A server sends a peer only the codes that peer’s revision defines.
  • _meta keys under io.modelcontextprotocol/ are reserved from 2025-11-25. In 2025-06-18 that prefix was not reserved, so a peer on that revision may use it for its own data. Send reserved keys only to modern peers.

4.2 Cancellation

  • Legacy. Either side may send notifications/cancelled, but never for initialize. The receiver SHOULD stop work and MUST NOT respond afterwards. A late or unknown cancellation is ignored.

  • 2026-07-28. Cancellation depends on the transport:

    • HTTP: closing the response stream is the cancellation. The server MUST treat a disconnect as cancellation and MUST NOT send further messages for that request.
    • stdio: the client sends notifications/cancelled.
    • Server-sent cancellation: a server MUST NOT send notifications/cancelled for anything except ending a subscriptions/listen stream.
  • Subscription teardown is contradictory in 2026.

    • The cancellation page: the server MUST send notifications/cancelled referencing the listen id.
    • The subscriptions page: it SHOULD send a successful subscriptions/listen result instead.
    • The CancelledNotification doc comment: the server-sent cancel happens on stdio.

    Only one of the three is a MUST, and only one ordering satisfies it: a cancellation may name only a request that is still in progress, so the notification has to go first and the result after it. That also satisfies the SHOULD, and it costs nothing — a client that acted on the cancellation ignores a late response, exactly as the cancellation page tells it to, and a client that waits for the result still gets it. The schema’s “on stdio” then reads as the case it was written for rather than as a restriction, because on HTTP closing the SSE stream is a second, redundant signal, not a substitute for the one the MUST names. mcpx sends both on both transports (internal/mcpserver/listen.go, announceEnd). The quotes are in conflicts C-21.

  • A proxy has to propagate cancellation. When the downstream cancels, the proxy cancels upstream:

    • legacy upstream: notifications/cancelled;
    • modern HTTP upstream: close the stream;
    • modern stdio upstream: notifications/cancelled.

    An upstream call parked behind MRTR has no downstream stream to watch. Its lifetime ends only when its requestState or task TTL expires.

4.3 Progress, logging, pagination, completion, ping

  • Progress. Sent only for a progressToken the client supplied, and progress MUST increase. In 2026 it travels only on that request’s stream.
  • Logging.
    • Legacy: sent only if the logging capability was declared. Before any logging/setLevel a server MAY send at its own level.
    • 2026: logging is deprecated, logging/setLevel is removed, and the level arrives in the request’s _meta. A server MUST NOT log for a request that set no level.
    • A dual-era server tracks the setLevel value per legacy session and ignores it for modern requests.
  • Pagination.
    • Cursors are opaque. An invalid cursor SHOULD get -32602, not a silent restart from the first page.
    • In 2026 every page of one list MUST carry the same cacheScope, and a client MUST NOT treat an empty-string cursor as the end.
    • An aggregating server’s cursors are stable only if its merged order is deterministic, and the 2026 changelog asks for deterministic tools/list order.
  • Completion.
    • Declare completions.
    • Return at most 100 values, sorted by relevance.
    • Apply rate limiting on the server side.
  • Ping.
    • Legacy: a server MUST answer promptly, and MAY send pings of its own.
    • 2026 removes ping, and servers may not send requests at all. Liveness becomes process liveness (stdio) and per-request success (HTTP). server/discover is the cheap probe.

4.4 Multi-round-trip requests (MRTR, 2026-07-28)

A modern server that needs input from the client (elicitation, sampling or roots) returns resultType: "input_required". The result carries inputRequests and an opaque requestState, and the client retries the same method under a new id with inputResponses and the requestState.

  • input_required is allowed only on tools/call, prompts/get and resources/read. A question that arises during any other method cannot reach a modern client.
  • The server MUST NOT put an input request in inputRequests unless that request declared the matching client capability. A retry carries its own capabilities, and the check is repeated on every round.
  • The server MUST NOT assume the retry will come. It MUST treat requestState as attacker-controlled, and SHOULD make it tamper-evident and bound to the principal, an expiry, and the originating method and parameters (§1).
  • If the call cannot proceed without a capability the client did not declare, the server answers -32021 with data.requiredCapabilities. A server with a fallback does not have to, but then it owes an eventual result or error, never a hang.

4.5 Tasks

  • 2025-11-25 (core).
    • A server that declares tasks.requests.tools.call MUST respect each tool’s execution.taskSupport, and a tool without it is forbidden by default. So the capability alone enables nothing: a server has to mark tools.
    • Task ids MUST be unique.
    • tasks/result MUST block until the task is terminal, and MUST carry io.modelcontextprotocol/related-task.
    • A cancelled task MUST stay cancelled.
    • tasks/cancel on a terminal task MUST return -32602.
    • tasks/list must be scoped by authorization context.
  • 2026-07-28 (extension io.modelcontextprotocol/tasks).
    • The server returns resultType: "task", and the result arrives on tasks/get.
    • A task must be durably created before the response is sent.
    • A server must never return a task to a client that did not declare the extension. resultType: "task" is the only thing that distinguishes a task handle from a real result, and a client that did not negotiate the extension has no schema for it — so returning one unasked hands it a result it cannot parse. The changelog’s “unsolicited” reads otherwise; see conflicts C-29. This is also why tasks/list is absent from the extension: polling a client-hosted task would itself be the server→client request 2026-07-28 forbids (../compare/register/capabilities.md).
    • The extension’s methods from a client that did not declare it are -32021 with data.requiredCapabilities.
  • What the prose implies. A task handle outlives the request that created it, and the connection. Task state cannot live in connection or session state, and tasks/list visibility cannot be keyed by connection. “Durably” is stronger than “in memory”. A pool of processes serving one configuration must either share the task store or route tasks/* to the owning process.

4.6 Caching (2026-07-28)

Cacheable results carry ttlMs and cacheScope, and both are REQUIRED: server/discover, tools/list, resources/list, resources/templates/list, prompts/list and resources/read.

  • A private result MUST NOT be served to another authorization context. A gateway is the “shared” cache the schema names.
  • An aggregator computes its own scope: private if any contributing upstream page is private. Its ttlMs should not exceed the smallest upstream TTL.
  • Lists MUST NOT vary per connection.

4.7 Subscriptions (2026-07-28)

subscriptions/listen replaces resources/subscribe, the legacy GET stream and unsolicited list_changed notifications.

  • The request carries a notifications filter.
  • The server acknowledges with notifications/subscriptions/acknowledged, which echoes the honoured filter. The subscription id travels in _meta, and the acknowledgement MUST be the first message for that subscription.
  • A server MUST NOT send a notification type the filter did not request. Every notification on the stream carries _meta["io.modelcontextprotocol/subscriptionId"].
  • A client may hold several concurrent listens, keyed by the listen request’s id. Each has its own filter, acknowledgement and teardown. The server keeps listenId → {filter, stream} and fans each event out to every matching stream. A new listen must never replace an existing one.
  • On stdio the tag is the only way to tell streams apart. On HTTP each listen is its own SSE response, and the tag is still required.
  • A listen request normally never gets a response, and the general “establish timeouts for all requests” rule has no exception for it. Keep-alives (SSE comments) exist so intermediaries do not time it out.
  • The 2026 resources page still says to send list_changed “when the list changes” with no mention of a listen stream. The schema’s filter rule takes precedence.

5. Features

Tools

  • Invalid input. Up to 2025-06-18, invalid input is a protocol error. From 2025-11-25 it is a tool result with isError: true, so the model can correct itself. An unknown tool is a protocol error (-32602) in every revision.
  • Structured output.
    • structuredContent and outputSchema arrive in 2025-06-18. With an outputSchema, the server MUST return conforming structuredContent, and SHOULD also include it as text.
    • Both must be objects in 2025-06-18 and 2025-11-25. 2026 allows any JSON value and any 2020-12 schema.
  • Names. 2025-11-25 adds constraints on tool names.
  • Headers. 2026 adds x-mcp-header parameters that are mirrored to headers. A server re-listing another server’s tools inherits their x-mcp-header annotations, and with them the obligation to validate the mirrored headers (§3.3).
  • Proxies. Human-in-the-loop confirmation is a host SHOULD, and a proxy must not claim it on the host’s behalf.

Resources

  • URIs are validated.
  • Not-found is -32002 (legacy) or -32602 (2026).
  • resources/updated goes only to clients that subscribed: through resources/subscribe in legacy revisions, and through a listen filter that names the resource in 2026.
  • The 2026 prompts page requires an embedded resource to carry a MIME type, but the schema makes mimeType optional. Send it.

Prompts

  • Arguments are validated.
  • 2026 is the only revision whose prompts page requires the capability to be declared in DiscoverResult. Declare it there regardless.

6. Accept liberally, send conservatively, field by field

A server that also serves older revisions must not send a peer anything its revision did not define. Unknown fields are usually ignored, but no revision guarantees it.

carriedsend tootherwise
resultType2026 requestsomit; accept absence as complete
structuredContent≥ 2025-06-18; must be an object for 2025-06-18 and 2025-11-25render into content as text
outputSchema with a non-object root2026withhold the tool’s schema, or wrap it
resource_link block≥ 2025-06-18embedded resource
audio block≥ 2025-03-26drop, or describe as text
tool annotations≥ 2025-03-26strip
title, resource lastModified≥ 2025-06-18strip
icons, description, websiteUrl on Implementation≥ 2025-11-25strip
ttlMs, cacheScope2026 (REQUIRED there)omit
serverInfo2026 in result _metaInitializeResult.serverInfo
io.modelcontextprotocol/* _meta keys≥ 2025-11-25, and only keys that revision definesomit
error codesonly those the peer’s revision definesnearest defined code
batchesnever sendaccept on input (2025-03-26 MUST; nothing forbids receiving them)
server → client requestslegacy peers that declared the capabilityMRTR inputRequests for 2026
list_changed, resources/updatedlegacy: the session; 2026: matching listen streams onlynothing
logslegacy: at or above the session level; 2026: only when the request set a levelnothing

Accepting is the other direction. A method beyond the negotiated revision (for example subscriptions/listen from a legacy client) withholds nothing, and answering it is not a violation. Declaring it is: a capability is a promise about the revision in force. For 2026 peers the answer is narrower, because an unknown method is -32601 with 404 on HTTP, and a modern peer should expect exactly the methods 2026 defines.


7. Where mcpx stands

Measured, not asserted: the official suite’s results, and which of its failures are defects, are official-suite.md; the per-requirement status is conformance-matrix.md. Two obligations on this page were corrected in mcpx because the suite found them: a modern request without the required _meta is -32602, not -32020 (a missing body field is not a header disagreeing with a body), and the five methods 2026-07-28 removed are -32601 (404 on HTTP) to a 2026-07-28 peer (#250; ../protocol.md §2.1).

Two readings on this page are mcpx decisions, recorded with their reasons in ../protocol.md: a tool whose upstream may ask a question the request cannot receive does not get -32021, because the broker answers instead (§3.1); and a legacy resources/subscribe for a URI whose updates cannot be delivered succeeds, with a warning event, because the legacy revisions have no way to say “agreed, but nothing will come” (§4.3, #251).

mcpx as a server: the transport

What mcpx serve (stdio) and the daemon’s /mcp (Streamable HTTP) do on the wire, revision by revision, and why. Everything here is tested; the test names are <revision>/<area>/<requirement> in internal/mcpserver/transport_test.go and internal/e2e/transport_test.go.

mcpx serves both eras at once: 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, which negotiate with initialize (2024-11-05 over stdio and Streamable HTTP; its own HTTP+SSE server transport is not hosted), and 2026-07-28, which carries its version in every request’s _meta. Over HTTP the two share one endpoint, so the first thing the transport does with a POST is decide which era wrote it.

Which era a POST belongs to

A POST is modern if its body carries _meta["io.modelcontextprotocol/protocolVersion"], or its MCP-Protocol-Version header names a revision at or after 2026-07-28 (a date mcpx does not implement still counts: that request is answered with -32022, not treated as legacy). Everything else is legacy.

The header is enough on its own because a modern header on a body without the _meta field is exactly the disagreement the modern header rules exist to catch, and it should be refused as that rather than quietly served as legacy.

Streamable HTTP, 2026-07-28

  • Mirrored headers. MCP-Protocol-Version and Mcp-Method are required on every request POST, Mcp-Name on tools/call, prompts/get and resources/read. Missing, different from the body, or carrying bytes a header may not carry (anything outside visible ASCII, space and tab) is 400 with -32020 HeaderMismatch and the request’s id. Mcp-Name in the =?base64?…?= sentinel is decoded before the comparison; the Base64 is strict – padding required, alphabet only – as the specification’s test-case table requires. Header names are matched case-insensitively (Go canonicalises them), values case-sensitively. A notification POST has no header requirements in this revision, so none are demanded, but any it sends must still agree.
  • Mcp-Param-* headers. On tools/call, every x-mcp-header annotation in the named tool’s inputSchema – mcpx re-lists upstream schemas verbatim, so an upstream’s annotation counts – is checked: a header that carries bytes a header may not, fails to decode, differs from the body value, is missing while the body has a value, or is present while the body has none, is 400 with -32020. Integers compare numerically. The rules live in internal/mcpheaders, shared with the client side that sends these headers.
  • Status is a function of the response, in one place (modernStatus): -32022, -32021, -32020, -32602, -32600, -32700 are 400; -32601 is 404; everything else 200. Every -32602 is 400, not only the missing-_meta one the spec names, because a status that depended on which handler produced an invalid-params error would not be a function of the response, and every invalid-params error is a client error.
  • No sessions. An Mcp-Session-Id on a modern request never binds it to a legacy session: an unknown one is ignored (no 404), and none is echoed or minted – server/discover used to mint one so a requestState had something to bind to; it is now bound to the request instead (messages.md).
  • No batches. An array body is 400 -32600.
  • Notifications are 202 with no body; one that is not accepted is 400 with an id-less error. An unknown notification method is ignored (202): JSON-RPC gives a notification no reply, and every revision says to ignore what is not understood.
  • Closing the stream is cancellation. The request runs under the HTTP request’s context, so a client that disconnects cancels the upstream call (the test asserts the backend’s context ends, not only the handler’s), and nothing is written afterwards.
  • No Last-Event-ID, no SSE event ids.

Streamable HTTP, 2025-03-26 … 2025-11-25

  • Sessions are minted by initialize. A request naming an unknown, expired or deleted session is 404, which tells the client to initialize again. DELETE with a session ends it and closes its GET stream (204); without one it is 400 (mcpx does allow termination, so 405 would be wrong); for an unknown one 404. A legacy request with no session header is served statelessly, as before.
  • MCP-Protocol-Version present and not a legacy revision mcpx implements is 400 with -32022 and the legacy list. Absent means 2025-03-26 for a sessionless request. In a session, the version negotiated at initialize governs even if the header names another supported one: the spec says the client SHOULD send the negotiated version and the server may rely on “the protocol version negotiated during initialization”, so a disagreeing header is a client quirk to tolerate, not a reason to refuse.
  • Batches exist only in 2025-03-26, where a server MUST accept them; 2025-06-18 removed them. So a batch is accepted when the session negotiated 2025-03-26, or a sessionless request has no header or a 2025-03-26 one, and is otherwise 400 -32600. Elements run concurrently; responses in a batch are delivered to waiting questions; the reply is a JSON array (or the elements’ replies as separate events if something had to stream); a batch of only notifications and responses gets 202. initialize inside a batch is refused as that element (2025-03-26 lifecycle: it MUST NOT be batched), and mints nothing.
  • GET without a session, or with a modern header, is 405; with an unknown session 404; otherwise it opens the session’s notification stream (text/event-stream, X-Accel-Buffering: no). One per session: a second is 409, because the spec forbids broadcasting a message across streams and a stray second stream would silently take half the notifications. The stream carries a : comment every transport.sseKeepAlive, closes on DELETE, and carries notifications/{tools,prompts,resources}/list_changed (which a legacy client opts into simply by mcpx declaring listChanged) and notifications/resources/updated for resources/subscribe. A subscription made before the stream opens delivers once it does; with no stream open a notification is dropped, since mcpx keeps no redelivery buffer and the legacy transport lets a client choose not to listen.
  • Cancellation. In a session, a dropped connection is not a cancellation — the legacy revisions say it SHOULD NOT be read as one — and notifications/cancelled posted on the session cancels the request’s context and withholds its reply (the POST ends as an event stream with nothing on it, the only shape that carries no response). Without a session no cancellation could ever reach the request, so there the disconnect remains the signal.
  • Responses and notifications: 202 no body; a response nothing is waiting for is 400.

Origin (every revision with Streamable HTTP)

A request whose Origin is present and not allowed is 403 with an id-less JSON-RPC error, on every method. Allowed: no Origin at all (every non-browser client), the loopback hosts (transport defaults: localhost, 127.0.0.1, ::1) at any port over http or https, the daemon’s own daemon.address when it is a specific address, and the exact origins in transport.allowedOrigins. The daemon’s address is named rather than taken from the request’s Host, which is precisely what DNS rebinding controls. Origin: null is refused: it names nobody.

stdio, every revision

  • Requests run concurrently, each under its own context. Before, they ran in line, so a notifications/cancelled for a running request could not be read until that request had finished and cancellation did nothing. notifications/cancelled now cancels the context (which reaches the upstream call) and the reply is suppressed. Notifications and initialize stay in line: a notification must land before whatever follows it, and initialize settles what every later reply looks like. Writes are serialised, one frame per line. Replies are no longer in request order, which JSON-RPC never promised; the one test that assumed it now matches by id.
  • Notifications are never answered, not even with an error.
  • Batches: the same per-version rule as HTTP, from the connection’s negotiated version (or 2025-03-26 before any initialize); the reply is an array, nothing for notifications only, and initialize in a batch is an error element.
  • EOF stops reading. Requests already in flight get transport.stdioDrain to answer, then are cancelled; the process then exits. A host that closes stdin after writing its requests still gets their answers.
  • Before initialize: mcpx is dual-era and accepts liberally, so ping (and anything else) before initialize is answered under the oldest revision’s shapes. A modern client sends no initialize at all, and refusing the legacy ones would make the stdio probe order matter for no gain.
  • stdout carries only MCP. Checked against the real binary: initialize, list, a successful call with a newline in its text, a tool error, an unknown tool and an unknown method, and every stdout line must be one JSON-RPC message.
  • After a legacy initialize, list_changed is forwarded as on the GET stream: it was declared, so it must be delivered.

Found along the way

  • A legacy client was declared listChanged: true on stdio and never sent a single list_changed: nothing started a notifier for it. Only resources/subscribe and the modern subscriptions/listen did.
  • Over HTTP a legacy session declared neither subscribe nor listChanged, honestly, because there was no GET stream to deliver on; now there is, and both are declared and delivered.
  • stdio answered a notification with an unknown method with a method-not-found error carrying no id.
  • The previous header check compared MCP-Protocol-Version with _meta only when both were present and answered with a non-JSON-RPC body, so a modern POST with no headers at all was served.