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

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.