Pi vs OpenCode vs iii for extensible coding workflows

You've built a small HTTP service that returns release notes for versioned packages, and now both your coding agent and your application need to call it.
Every team building real workflows hits this extension decision eventually, and it's where the top Pi vs OpenCode comparisons go silent.
The top-ranking comparison pages contrast OpenCode's built-in MCP with Pi's extension-first core, but none of them adds the same tool to both agents and asks what happens when the application also needs that tool.
In this article, we built one release-notes lookup as a standalone HTTP service, connected it with Pi, OpenCode, and iii, and tested how each tool works when your agent and your app both need the same tool.
Versions tested, checked October 13, 2026: Pi 1.0.4, OpenCode v2.0.24, and iii engine 0.24.4 with AI Agent Harness 1.8.44, all running against the same claude-sonnet-5 model via the same Anthropic API key.
Our quick take by workload
Pi and OpenCode are coding agents you extend with MCP servers or plugins. iii is a framework where the agent and the application call the same Functions. The right choice depends on which problem you're actually solving.
Best for getting the tool call right with minimal config
Pi
The .pi/mcp.json wiring was the smallest config of the three (one pi mcp add command, one JSON file), and the agent used the tool correctly with no missteps in our run.
Pi in iiiBest for a full agent with plugins and MCP built in
OpenCode
OpenCode ships agents, commands, skills, MCP, and a plugin API in one install, so you're customizing one complete tool rather than assembling pieces.
OpenCode in iiiBest when the app needs the tool too
iii
Confirmed in our run: the app called the exact same release-notes::get Function the agent did, with no second implementation, and both calls appeared in the same filterable trace list in the console.
Start the harnessHow we ran the test
We built one release-notes lookup as a standalone HTTP service returning merged changes for a beacon-cli version from a local fixture, with a clean 404 for unknown versions. We connected it three ways: a stdio MCP server used identically by Pi and OpenCode, and a Function registered by an iii Worker.
All three call into the same running service, so every result below reflects how each option exposes and reaches a tool.
We ran four cases per tool using the same fixed prompt template: "Look up the release notes for beacon-cli version {VERSION} using the available tool, and summarize the changes in one sentence."
| Setup | Pi | OpenCode | iii |
|---|---|---|---|
| Version tested | 1.0.4 | 2.0.24 | Engine 0.24.4, AI Agent Harness 1.8.44 |
| Where the tool runs | MCP server process | MCP server process | Worker process |
| How the agent finds it | .pi/, exposure direct | mcp in opencode, codemode: false | AI Agent Harness allow list (options) |
| How the app calls it | A separate script (app-client/), its own plain HTTP call with no shared code with the agent's MCP server | Same separate script, same way | iii trigger release-notes::, the identical registered Function the AI Agent Harness exposes to the agent |
| Model and key | claude-sonnet-5, Anthropic API key | Same | Same |
Two coding agents you extend and one framework you build on
All three options can call a tool running outside the agent's own process. The difference is how you declare the tool, who else can call it, and what you own after it's working.
The sections below cover where custom tools run and how the application reaches them. For approval gating and permission policy, see OpenCode vs Claude Code vs iii for human in the loop workflows.
Pi
Pi is Earendil's MIT-licensed "minimal, extensible agent harness." Pi 1.0 shipped October 1, 2026, and extensions are TypeScript modules that load without a build step. Of the three options, Pi had the smallest config surface to go from a running service to an agent that could call it.
Key facts
- Version tested: 1.0.4
- License: MIT
- Where custom tools run: an MCP server process started by Pi via
.pi/(the route used in this test), or inside the Pi process itself viamcp .json piand.registerTool pi(not exercised here, per Pi's Extensions docs).registerMcpServer - How the app reaches the same tool: a separate script making its own plain HTTP call, with zero code shared with the MCP server
- How extensions are shared: Pi packages install from npm or git, with versions that can be pinned, per Pi's Packages docs
Why it stands out
pi mcp add <server> -l --exposure direct -- <command> wrote a working, trusted-on-first-run MCP config in one command. Pi was the only tool of the three where wiring the shared service in was a single CLI invocation rather than a JSON block we hand-edited.
The tool_call handler in Pi's extension API can also change tool input or block execution before the model sees the result, per Pi's docs, though we didn't exercise that path in this run.
Limitation
Project-local MCP config is untrusted by default and silently ignored until the project is trusted, confirmed directly.
Our first pi mcp list attempt showed nothing configured until we ran pi --approve. Extensions run inside the Pi process with the host OS's permissions. Pi's Security docs point to a container, VM, or sandbox for real isolation, with no built-in boundary.
Best for
Developers who want a small config surface to get one shared tool working and are comfortable writing the application's own call path separately. Follow the iii install guide to start the AI Agent Harness and console on your machine.
OpenCode
OpenCode is Anomaly's MIT-licensed coding agent. OpenCode 2 ships agents, commands, skills, MCP, and a plugin API built on transforms and hooks.
Of the three, OpenCode gave us the fastest live config turnaround, and the only unexpected model behavior worth calling out.
Key facts
- Version tested: v2.0.24
- License: MIT
- Where custom tools run: an MCP server process declared in
opencodewith.jsonc codemode: false(the route used in this test), or inside an OpenCode plugin registered viactx(per OpenCode 2's Build plugins docs).tool .transform - How the app reaches the same tool: the same separate script used for Pi, a plain HTTP call to the service with no shared code with the agent's MCP connection
- How plugins are shared: plugins install from npm or git and are listed in
opencode, per OpenCode 2's Configure plugins docs.json(c)
Why it stands out
A new MCP server entry took effect after opencode reload, confirmed via opencode mcp list showing the new server connected.
OpenCode also produced the only unexpected model behavior in the test. On the known-version prompt, the agent reached for its own built-in web search tool before the correctly configured release-notes_get_release_notes tool.
The web search errored (no search capability configured in the sandbox), and OpenCode then correctly fell back to the right tool and succeeded. Registering a tool doesn't guarantee the model picks it first.
Limitation
OpenCode 1 plugins do not run in OpenCode 2, per OpenCode 2's Migrate from V1 docs.
Best for
Teams that want a complete agent and plan to iterate on its config or plugins during a live session, without restarting a background service. Follow the iii install guide to start the AI Agent Harness and console on your machine.
iii
Disclosure. iii is our product, included here because it answers the same question from the application side.
iii is a framework for building intelligent systems. Functionality is built once, and the AI Agent Harness, Triggers, and application code all call it.
Key facts
- Versions tested: engine 0.24.4, AI Agent Harness 1.8.44
- License: engine and CLI under Elastic License 2.0 (not OSI-approved); SDKs, console, and published workers under Apache 2.0
- Where custom tools run: a process registered via
worker-compose.yaml - How the app reaches the same tool:
iii trigger release-notes::, calling the identical Function the AI Agent Harness exposes to the agent. Workers are added viaget iii trigger compose::and pinned inadd worker-compose. Functions can be registered from the Node, Python, Rust, or Go SDK.lock
Why it stands out
iii was the only option of the three where the "app call" and "agent call" tests used the same Function definition rather than a second hand-written script.
Both calls appeared side by side in one trace view in the console, filterable by function name, each with its own nested span for the outbound HTTP call to the release-notes service.
The console's Traces page showed the agent's call nested inside the AI Agent Harness turn and the app's call as its own top-level trace from iii-node, making the full call history of that Function visible in one place.
Limitation
Worker code changes require an explicit iii trigger compose:: before the new behavior is visible. On Windows, bundle Workers need WSL2 with /.
Best for
Teams building an application where agents and code share tools. In Pi and OpenCode, a custom tool is something the agent connects to. In iii, it's first-class functionality.
In iii, a Function is something the application already owns, and the agent is one more caller of it. Follow the iii install guide to start the AI Agent Harness and console on your machine.
What happened when the agent and the app shared one tool
Where the tool runs and who else can call it
The MCP route (.pi/, exposure: direct) is what the Pi agent uses. The application couldn't reuse that connection and needed its own code path that shares zero code with the MCP server.
The underlying HTTP call is identical either way, so we wrote it once and reused it for both Pi's and OpenCode's app path.
OpenCode's agent MCP config (opencode, codemode: false) is agent-only. The application used the identical separate script Pi's app path used, making a plain HTTP call to the service.
With iii, the Worker registers release-notes:: once. The AI Agent Harness's allow list exposes it to the agent, and iii trigger exposes the exact same Function to the application. We confirmed this in the console's trace list, where both call paths appear under the same function name: one nested inside a harness:: trace (worker: harness), the other as its own top-level trace (worker: iii-node).
What the model saw when the tool failed
Stopping the release-notes service mid-session and asking for a known version produced the same shape of answer from all three agents: a clear unreachable explanation, no retry, no fabricated changelog.
Pi's tool result carried isError: true with the text "release-notes service is unreachable at http://127.0.0.1:4455: fetch failed". The model relayed that plainly and asked whether to retry.
OpenCode's tool result carried the identical underlying error text. The model explained it and offered an alternative (a GitHub repo link) rather than guessing.
iii's agent path returned a function_result with is_error: true. The AI Agent Harness turn's own result explained the same failure in one sentence. The application path returned a structured {code, message, stacktrace} error from the CLI, a different shape than the agent received.
Asking for an unknown version (9.9.9) with the service running produced equally clean failures from all three. "No release notes for version 9.9.9" surfaced to the model via the tool or function result in each case, and all three explained it rather than inventing a changelog.
Where arguments get checked. All three tools pass the version argument to our code without prior validation. The request schema registered with each (Pi's tool schema, OpenCode's MCP tool schema, iii's request_format) is descriptive only. The actual string-type and non-empty check happens inside our own handler, consistent with what MCP's protocol does for Pi and OpenCode and confirmed directly for iii in this run.
Where each failure is visible afterward. For Pi, the saved session JSONL (exportable to HTML via pi --export). For OpenCode, the session view in its paired browser UI, or the one-shot JSON event stream at call time. For iii, the console's Traces page for that call, including the nested outbound HTTP request that actually failed or returned a 404.
Table tested on 2026-10-13.
| Case | Pi | OpenCode | iii |
|---|---|---|---|
| Agent call works | Succeeded, with a correct one-sentence summary | Succeeded, after first trying (and failing at) its own built-in web search, then producing a correct summary | Succeeded, with a correct one-sentence summary nested inside the AI Agent Harness turn's trace |
| App call works | Succeeded via the app's own separate script (HTTP 200 + JSON) | Same separate script, same result | Succeeded via iii trigger release-notes::, the same Function the agent calls |
| Service stopped | Clean "unreachable" error relayed to the user, no retry | Clean "unreachable" error relayed, with an alternative offered, no retry | Agent: clean one-sentence failure explanation. App: structured {code, message, stacktrace} error from the CLI |
| Unknown version | Clean "no release notes for version 9.9.9" relayed, no guessing | Same, with a typo/non-existent-version explanation, no guessing | Agent: clean failure explanation. App: same structured error shape as the service-stopped case |
| Where the failure shows up | The session JSONL or its HTML export | The session view in OpenCode's paired browser UI, or the JSON event stream at call time | The console's Traces page for that call, including the specific failed or 404'd outbound HTTP request |
What you maintain after it works
With Pi, you hand-maintain one file, .pi/, plus the application's own separate script. A change to the MCP server list is picked up by / inside an active session (documented, though not directly exercised in --print mode, which has no session to send / into).
With OpenCode, you maintain opencode, plus the same separate application script. A new MCP server entry takes effect after opencode reload, with no service restart.
With iii, you maintain the worker-compose entry plus the worker's own directory (worker, package, iii). A code change inside the worker did not take effect until iii trigger compose:: was run. Querying immediately after the edit, before the restart, still returned the old behavior. With additional configuration, iii does support file-watching in worker start scripts via tsx watch or equivalent.
Table tested on 2026-10-13.
| Lifecycle | Pi | OpenCode | iii |
|---|---|---|---|
| Extension code you own | One MCP config entry + the app's own script | One MCP config entry + the same app script | A worker directory (worker, package, iii) + one worker-compose entry |
| Where it is configured | .pi/ or extension | opencode or plugin | worker-compose |
| How a change loads | / in an active session (documented, not directly exercised in --print mode) | opencode reload, confirmed live, no service restart | iii trigger compose::, confirmed required, with no automatic hot reload for worker code without a file-watching start script |
| What the last major version broke | Move to 1.0 after MCP landed in v0.99.0 (not re-tested, per maker changelog) | V1 plugins do not run in V2 (not re-tested, per maker docs) | iii worker add removed in engine 0.23, replaced by compose:: |
| How a teammate installs it | Pi package or .pi/ | Plugin package or opencode | worker-compose and lock |
Process separation and security boundaries
Separate processes don't create security boundaries. All three tools behaved the same way in both failure and success cases, regardless of process isolation. The service being a separate process changed what crashed when we stopped it (the service alone, not the agent or the AI Agent Harness), but it didn't change what any of the three tools' own code could reach.
Pi's docs say extensions run with the same OS permissions as Pi itself and point to a container, VM, or sandbox for real isolation.
OpenCode's docs warn that shell commands run with the host user's own filesystem, process, and network authority.
iii's rbac-proxy Worker decides which callers may invoke which Functions, but it is not an isolation boundary. Sandbox calls and Compose bundles run in VMs, but a local Worker like the one in this test runs on the host unless configured otherwise, per the iii Sandboxes docs.
Choose where a tool lives based on reuse and failure handling, which is what this test measured, then choose isolation separately. For approval gating and how to control what a coding agent can do before it does it, see What stops your coding agent from doing something it can't undo?
Where iii fits
You can keep Pi or OpenCode and add iii around them. The pi worker exposes Pi as pi:: and the opencode worker exposes OpenCode as opencode::, mirroring each agent's lifecycle events into iii's engine so the console traces them alongside native AI Agent Harness turns.
The mcp worker can expose iii Functions to MCP clients, which now include Pi and OpenCode. We describe this as a compatibility path: tools already built as iii Functions can be surfaced to agents that connect via MCP.
iii supplies the framework and routes invocations across Workers, with inference coming from the provider you connect, whether Anthropic, OpenAI, or another.
What else to know before you choose
Does Pi support MCP servers now?
Yes. Pi added MCP with Codemode in v0.99.0 on September 29, 2026, and Pi 1.0 ships it. Earlier reviews, including the AI Overview for the primary search term at research time, contrasted OpenCode's built-in MCP with Pi's extension-first core. That distinction is now historical, as both ship MCP as a first-class configuration path.
Can I embed Pi or OpenCode in my own app?
Yes, in different shapes. Pi offers a TypeScript SDK and an RPC mode over stdin and stdout. OpenCode offers a client for its background service's HTTP API, per OpenCode 2's client docs. In iii, the AI Agent Harness is itself a Worker on the same engine as your application's Workers, so the integration boundary is a Function call rather than a separate protocol.
How do I share an extension with my team?
Pi packages bundle extensions, skills, and prompt templates from npm or git, and versions can be pinned, per Pi's Packages docs. OpenCode plugins install from npm or git and are listed in opencode. iii Workers come from the registry via compose:: and are pinned in worker-compose.