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
/v1route 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.jsonand in the documentmcpx openapiprints — one document now, generated from the table; the stale hand-written serve-surface document it replaced is recorded in the header ofinternal/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()ininternal/cli/manual.godeclares 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.