Skip to content

RFC: Plugin lifecycle management in ToolHive#77

Open
JAORMX wants to merge 8 commits into
mainfrom
rfc-plugins-lifecycle-management
Open

RFC: Plugin lifecycle management in ToolHive#77
JAORMX wants to merge 8 commits into
mainfrom
rfc-plugins-lifecycle-management

Conversation

@JAORMX

@JAORMX JAORMX commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Proposes adding plugin lifecycle management to ToolHive, mirroring the existing skills system (RFC-0030). A plugin is the cross-vendor "bundle of primitives" unit (slash commands, subagents, Agent Skills, hooks, MCP server configs, LSP servers) declared by .claude-plugin/plugin.json.

ToolHive will let users build a plugin directory into a reproducible OCI artifact, push it to any OCI registry, install it (from registry name / OCI ref / git://), and list/info/uninstall it — reusing the registry, OCI, groups, and SQLite entries infrastructure that already serves skills. As a bridge to native client tooling, it can also generate a marketplace.json.

Why

  • Plugins are the industry-convergent bundle unit (Claude Code, Cursor, Codex, Copilot, Gemini, Kiro), but no one packages a multi-primitive plugin bundle as an immutable, content-addressable, signable OCI artifact — an open niche ToolHive is positioned to fill.
  • The skills system was deliberately built on a generic foundation (entry_type discriminator, x/dev.toolhive/<type> registry namespace, shared OCI primitives) that anticipates exactly this extension.

What's genuinely new vs. skills

  1. Install target — plugins normally need marketplace-cache + settings.json mutation. The RFC recommends the in-place skills-directory-plugin mechanism for v1 (pure filesystem, no settings mutation), with marketplace-cache deferred as a per-client PathResolver strategy. (Open Question Port THV-0597 #1)
  2. Executable surface — unlike inert skills, plugins bundle hooks and MCP servers (code). The security section centers on this: a pre-install executable-surface inventory in thv plugin info, --require-signature via cosign + the OCI Referrers API, and install-time audit events.

Notes for reviewers

  • File is named THV-XXXX-... per the RFC convention; will rename to match this PR number.
  • Touches toolhive (primary), toolhive-core (new oci/plugins + shared-primitive refactor), and toolhive-registry-server (plugin catalog).
  • Key product decisions to weigh: Open Question Port THV-0597 #1 (install target) and Port THV-1566 #4 (running bundled MCP servers through thv with isolation as the v2 headline).

🤖 Generated with Claude Code

JAORMX and others added 6 commits June 15, 2026 08:04
Proposes adding plugin (multi-primitive bundle) lifecycle management to
ToolHive, mirroring the existing skills system: build, push, install,
uninstall, list, info, validate, and marketplace generation. Plugins are
packaged as reproducible OCI artifacts (dev.toolhive.plugins.v1), reusing
the shared toolhive-core OCI primitives, the SQLite entries table, the
registry provider seam, groups, the git resolver, and the multi-client
PathResolver.

Centers on what makes plugins different from skills: an executable surface
(hooks + bundled MCP servers), addressed via a pre-install component
inventory, signature verification over the OCI Referrers API, and
install-time audit events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dotted-edge label collided with the .-> arrow syntax; angle brackets and
special characters in flowchart labels broke the lexer. Use pipe-label form
and quote labels containing special characters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make explicit (verified against current Claude Code docs) that a
skills-directory plugin activates all components — commands, agents, hooks,
MCP, LSP, skills — not just skills. Add project-scope trust caveats
(MCP per-server approval, LSP trust, monitors don't load, no repo-root
walk-up) and note the session-only --plugin-dir/--plugin-url alternatives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alization

Plugin bundle formats never converged (unlike skills' SKILL.md standard), so
a single tree does not install across clients. Reframe the design into two
layers: a fully client-agnostic distribution layer (build/OCI/push/catalog/
pull/verify/inventory) and a per-client materialization layer behind a
MaterializationAdapter seam. Scope v1 materialization to the .claude-plugin
family (Claude Code + Codex), with Cursor/Copilot/Gemini as future adapters.
Add the "plugin formats did not converge" design constraint, the adapter
interface, a multi-manifest alternative, and corrected goals/non-goals/
open-questions/forward-compatibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x source)

Codex does NOT auto-discover in-place: loading requires a cache install plus
a [plugins.*] enabled entry in ~/.codex/config.toml, and Codex activates only
a subset of components (skills/MCP/apps/hooks — no commands or subagents).
Model Codex as a distinct v1 adapter: cache-install + surgical config.toml
round-trip edit, SupportedComponents + install-time warning on dropped
commands/agents, revert-on-uninstall. Add an adapter comparison table, a
client-config-mutation security note, and update goals/summary/open-questions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JAORMX

JAORMX commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

So I went and did the cross-client research to sanity-check this (Claude Code, Cursor, Codex, Copilot, Gemini, Kiro, Continue, Goose, Zed, OpenCode, plus MCPB / Agent Skills / the OCI side). Two things came out of it: the factual claims mostly hold up, and a bigger structural problem that I think we got backwards. The structural one first, because it matters more.

This RFC is written through the skills lens: distribute an artifact, place the files where the client loads them, done. That works for skills because a skill is inert Markdown with no runtime. But a plugin isn't inert. Its .mcp.json is a set of MCP servers, and MCP servers are exactly the thing ToolHive exists to run... in a container, behind the proxy, with a permission profile, network isolation, secrets and audit.

So if we "install" a plugin by dropping its .mcp.json into the client's load path (skills-dir, marketplace cache, whatever materialization we land on), the client spawns those servers itself, unsandboxed, and we've bypassed the entire reason ToolHive exists. That's the opposite of what we want.

Right now the RFC files "managed MCP servers from plugins" under Non-Goals and Open Question #4, as a forward-looking v2 thing. I think that's inverted. For ToolHive specifically, running the bundled MCP servers through the runtime is the v1 thesis. It's the only part of a plugin install that a git clone + native install doesn't already give you. Everything else (commands, agents, skills, hooks-as-files) is supporting cast.

Concrete recommendation, and to be clear up front: this is an install-time thing, not a packaging one. The OCI artifact stays exactly what the RFC already says, one opaque, verbatim, single-layer, signable tree. I'm not touching that, and per-primitive layers (Alternative 2) are still the wrong call for v1. What changes is materialization: instead of placing the whole tree verbatim into the client's load path, treat components differently by their nature.

  • Inert components (commands, agents, skills, hooks, LSP config): materialize into the client's load path as-is. The skills model genuinely fits here, and this is where the placement-mechanism choice lives... which is honestly the low-stakes half.
  • MCP servers (.mcp.json): don't hand the raw spawn config to the client. Register each as a ToolHive workload, then rewrite the materialized client config to point at our proxy URL, exactly like pkg/client/converter.go already does for every other MCP server. The plugin's .mcp.json becomes a source of workload definitions, nothing more.

One honest consequence: this breaks the RFC's "byte-identical to a native install" compatibility goal, but only for .mcp.json. The inert components stay byte-identical to what you packaged; the MCP entries deliberately get rewritten to route through ToolHive. That's the whole point, so we should call it out as an intentional trade rather than pretend the install is a verbatim copy.

The genuinely hard v1 question, and the one the RFC should center on, is the gap between how plugins define servers and how we run them. Plugins use local command/args (node ${CLAUDE_PLUGIN_ROOT}/mcp/index.js). ToolHive runs containerized/remote workloads. So: do we run it as a proxied stdio local-process workload (middleware and audit, but weaker isolation)? Repackage into a container at build time? Offer both with a per-server policy? Plus the secrets angle (userConfig sensitive fields feeding the workload) and what default permission profile an untrusted bundled server gets.

On the materialization mechanism for the inert half: I'd drop the in-place skills-directory approach entirely. It's a discovery side-door meant for authoring (claude plugin init), it produces name@skills-dir rather than managed installs, it overlaps the skills directory, and project scope degrades in confusing ways (monitors don't load, no walk-up to the repo root, MCP needs per-server approval).

Go native instead. And the cleanest way to do that without standing up a daemon: we're already pulling and extracting the OCI artifact, so stage the bundle to a local ToolHive-managed dir and register that dir as a local-path marketplace source for the client. No URL server to keep alive, works offline, and we still own the supply chain because we verify the digest/signature before staging. The client then installs from the local path through its own native lifecycle (cache + enable). The "that's a lot of client state to own" worry doesn't really apply... registration + enabledPlugins is the same class of thing we already write for MCP servers and skills.

And note the bundle we stage isn't the raw one. The .mcp.json has already been rewritten to point at our proxy URLs (per the split above), so what the client's marketplace installs is the inert components plus MCP entries that route back through ToolHive. The local-path staging and the workload-registration aren't two separate features, they're the same install doing both halves.

Smaller factual fixes from the research, all verified against current docs:

  • Copilot's manifest path is .github/plugin/plugin.json (nested under plugin/), not .github/plugin.json. It's repeated in a few spots.
  • The Codex fallback to .claude-plugin/plugin.json is real, but only in the source (codex-rs/utils/plugins/src/plugin_namespace.rs), not the prose docs. Worth citing the code, otherwise a reviewer checking docs alone will mark it unverifiable.
  • cagent packages agents (it can bundle multi-agent teams), not "single agents"; the official MCP Registry points at npm/PyPI/MCPB/OCI, not just container images.
  • "distributed exclusively through Git/GitHub" misses npm as a native source type. The OCI-absence point still stands though, and it held up under a hard counterexample hunt: the MCP "Skills Over MCP" charter itself lists bundle-as-a-single-OCI-artifact as out of scope / not yet built.
  • Tekton doesn't key layers with org.opencontainers.image.title, that's Conftest. Tekton uses dev.tekton.image.{name,kind,apiVersion}.

The central differentiation claim (nobody packages a multi-primitive bundle as a signed OCI artifact) is solid, for what it's worth.

Happy to restructure the RFC around the bundle-decomposition thesis if we agree on it. That's a bigger rewrite than the factual patches, so flagging it before I touch anything.

JAORMX and others added 2 commits June 15, 2026 08:48
ToolHive already round-trip-edits ~/.codex/config.toml to register MCP servers
(pkg/client TOMLMapConfigUpdater + pkg/fileutils AtomicWriteFile/WithFileLock,
with a test proving hand-maintained fields survive). Reframe the Codex
adapter's config mutation from "riskiest piece" to reuse of well-trodden code;
narrow Open Question #1b to the local-marketplace-vs-direct-[plugins] choice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: v1 does not run/proxy/rewrite a plugin's bundled MCP servers.
The artifact carries .mcp.json verbatim but ToolHive ignores it; the intended
managed model references first-class ToolHive servers via the `requires`
mechanism rather than executing servers bundled in the plugin. Add a dedicated
"MCP servers in a plugin" section, Alternative 7 (run/repackage bundled MCP,
considered+deferred), reframe Open Question #4 and Forward Compatibility,
update Goals/Non-Goals/Summary/Security/mitigations, and add a deferred-work
note. Managed MCP via references = follow-up RFC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant