MCP gateway for agentic use. Agents can enable/disable specified MCPs based on their description when needed.
  • TypeScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Troed Sångberg 54af798315 fix: read the live mcp.list envelope and trace failures to a file
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.
2026-10-27 13:36:12 +00:00
docs/superpowers docs(oc-dyn-mcp): implementation plan for the MCP gateway 2026-10-27 12:23:42 +00:00
src fix: read the live mcp.list envelope and trace failures to a file 2026-10-27 13:36:12 +00:00
test fix: read the live mcp.list envelope and trace failures to a file 2026-10-27 13:36:12 +00:00
.gitignore feat: scaffold plugin and options parsing 2026-10-27 12:25:40 +00:00
index.ts feat: scaffold plugin and options parsing 2026-10-27 12:25:40 +00:00
package.json feat: scaffold plugin and options parsing 2026-10-27 12:25:40 +00:00
README.md fix: read the live mcp.list envelope and trace failures to a file 2026-10-27 13:36:12 +00:00
tsconfig.json feat: scaffold plugin and options parsing 2026-10-27 12:25:40 +00:00

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.servers is 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; reset restores 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, with action: list, enable, disable, reset (plus server where 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:

  1. mcp_gateway(action: "enable", server: "ghidramcp") — the call waits up to 10 s for the connection.
  2. 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 umwelt here) 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.list is 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 as unknown. 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.1 once 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.