- TypeScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The plugin's status reader recognised neither the {location, data: [...]}
envelope that ctx.mcp.list() actually returns nor a status record keyed
"status", so it read the envelope's own keys as servers and reported every
managed server as "unknown". Enabling therefore never confirmed, needs_auth
never produced sign-in guidance, and a failed enable never reverted; the
registry block claimed "on but unusable" for healthy servers.
Read the data envelope (array or map), unwrap status records keyed status as
well as type, and take a failure's error from the nested record. The adapter
test now fakes the real envelope instead of a flat shape that hid this.
OpenCode surfaces no plugin console output after the fact, so add src/log.ts:
a best-effort, queued, size-rotated logger at
$XDG_DATA_HOME/opencode/log/oc-dyn-mcp.log that records lifecycle events, one
mcp.list envelope sample per instance, and a warning when that envelope yields
no recognisable records.
|
||
| docs/superpowers | ||
| src | ||
| test | ||
| .gitignore | ||
| index.ts | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
oc-dyn-mcp
An OpenCode plugin that makes the MCP server set dynamic: a small gateway the agent can call to enable and disable allowlisted MCP servers mid-session, plus a compact registry block that tells it which servers exist and what they are for.
The problem
An MCP server's tools, prompts, and instructions are loaded into every session. Enabling every server that is
occasionally useful means paying for its tool definitions in every session; disabling it means the agent cannot reach
the capability at all, and cannot help itself — only the human can edit opencode.jsonc or use /mcps.
With this plugin, a rarely-used MCP costs one line of system text until it is needed. When a task needs it, the agent enables it and uses it in the next step, unattended.
How it works
-
options.serversis an allowlist: server name → one-line description. It is the only thing the plugin can touch. -
The initial state comes from
opencode.jsonc. Toggles are held in memory and are lost on restart or config reload;resetrestores the file's state on demand. -
Every request carries a registry block:
<mcp_gateway> MCP servers you can manage with the mcp_gateway tool. Servers marked "on" already have their tools in your tool list. on: tinysearch, reflect on but unusable: roster (needs sign-in) off — call mcp_gateway action "enable" with the server name when a task needs one: ghidramcp — Binary reverse engineering: decompile, disassemble, rename symbols, inspect data flow. Other MCP servers are not available to you through this gateway. </mcp_gateway> -
One tool,
mcp_gateway, withaction:list,enable,disable,reset(plusserverwhere it applies). Enabling is a real connect; disabling is a real disconnect.
Install
No build step, no dependencies — OpenCode loads the TypeScript directly.
{
"plugins": [
{
"package": "<path-to>/oc-dyn-mcp",
"options": {
"servers": {
"ghidramcp": "Binary reverse engineering: decompile, disassemble, rename symbols, inspect data flow.",
"context7": "Up-to-date library and framework documentation.",
"kubernetes": "Cluster inspection and pod operations."
}
}
}
]
}
The servers themselves stay normal mcp.servers entries; keep them "disabled": true if you want them off by default,
or leave them enabled and let the agent turn them off. Configuration reloads without restarting OpenCode.
The one-step rule
MCP tools join the model's next request, not its current message. So:
mcp_gateway(action: "enable", server: "ghidramcp")— the call waits up to 10 s for the connection.- Use
ghidramcp_*tools in the next step. Never in the same message as the enable call.
The tool result states this every time; a model that ignores it will see "unknown tool" for one step, which is the tool-snapshot behaviour of OpenCode rather than a gateway failure.
Status vocabulary
list and the block report what OpenCode reports: connected, pending, disabled, needs_auth, failed. A server
that needs sign-in is shown under on but unusable — authenticate it with /mcps. An enable that fails is reported and
reverted, so the registry never claims a server is on when it is not. A server whose config says on but which failed
can be retried by enabling it again.
Security posture
The allowlist is a boundary, not documentation:
- Only listed servers appear in the block, in
list, or in error text. Unlisted servers are invisible, and probing a name does not reveal whether it exists. - Every config write is derived from the allowlist, so a prompt-injected "enable kubernetes" is refused before any mutation happens.
- Do not list the MCP server that provides the agent's own tools or sandbox (for example
umwelthere) unless you accept an agent being able to shut it off mid-session. The allowlist governs both directions. - Gateway calls are subject to your normal permission rules.
Diagnostics
The plugin writes one line per lifecycle event to
~/.local/share/opencode/log/oc-dyn-mcp.log (or $XDG_DATA_HOME/opencode/log/oc-dyn-mcp.log), alongside the other
plugins' logs. OpenCode does not surface plugin console output anywhere readable afterwards, so the file is the only
durable trace:
[oc-dyn-mcp] 2026-10-27T13:31:47.135Z info gateway active for ghidramcp, context7, kubernetes
[oc-dyn-mcp] 2026-10-27T13:31:47.135Z info mcp.list = {"location":{...},"data":[{"name":"context7","status":{"status":"connected"}}]}
[oc-dyn-mcp] 2026-10-27T13:31:52.001Z warn could not read MCP status: fetch failed
- Setup, activation, config warnings and failures are recorded; successful toggles are not, so the file stays a lifecycle trail rather than a request stream.
mcp.listis sampled once per plugin instance: its envelope is the one piece of host data the plugin cannot verify any other way, and an unrecognised shape silently reads every status asunknown. An envelope that yields no recognisable records raises one warning.- A write never breaks the gateway: writes are queued, best-effort and swallowed on error. The file rotates to
.log.1once it passes 1 MiB.
What it does not do
No per-session or per-agent state, no persistence across restarts, no catalog caching or probing, no auto-enabling from
the prompt, no health monitoring. Those were considered and left out; see
docs/superpowers/specs/2026-10-27-mcp-gateway-design.md.
Development
node --test
The pure modules (config, status, gateway, render, poll, log) hold all the logic and are tested directly;
plugin.ts is a thin adapter tested against a fake context.