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

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