Design spec: Agent Plugins support (mdm plugins)¶
Historical document - mdm plugins is stable
This is the original design spec, written when the feature was still
being built, and it is kept unchanged as a record of that design. It
describes an experimental gate and a separate plugins-lock.json;
neither exists any more. In v2 the gate was removed, the command group
is always available with no opt-in, and its lock entries live in the
plugins section of mdm.lock (mdm migrate folds an old file in).
For how mdm plugins works today, read the
Agent Plugins guide and the
command reference.
| Status | Implemented - graduated to full support in v2 |
| Stability | Stable. Everything below describes the original experimental phase and is retained for history only. |
| Author | Dakota Kim |
| Created | 2026-08-06 |
| Tracking issue | TBD |
| External spec | Agent Plugins v1.0.0 |
Summary¶
Add an experimental mdm plugins command group that installs, validates, and
manages Agent Plugins - the vendor-neutral agent-plugins.org format for
packaging skills and MCP servers as portable plugin directories. The spec
deliberately defines no lockfile, registry, or install mechanism; that is the
gap a package manager fills. mdm reuses its existing acquisition, locking, and
security pipeline, ships the feature behind a named experimental gate, and
makes no stability promises while adoption of the standard is still forming.
A plugin directory contains a plugin.json manifest, skills under
skills/<name>/SKILL.md (the Agent Skills format mdm already parses), an
optional mcp.json describing MCP servers, and reverse-domain extension
directories for client-specific content.
Note this is not the same thing as Claude Code's
.claude-plugin/marketplace.json format, which internal/skill reads for
skill grouping - that is a marketplace index; agent-plugins.org defines the
plugin package itself.
Goals¶
- Install and manage plugins from the same source types skills support, pinned in a lock file, restorable in CI.
- Full spec conformance as a client: closed-schema manifest validation
without fetching schemas, fixed-location component discovery, path
containment, and the resilience rules (a broken
mcp.jsondisables MCP only; a broken server or skill is skipped individually). - MCP wiring: translate
mcp.jsoninto each harness's native MCP config (Claude Code's.mcp.json, Cursor's.cursor/mcp.json), performing the launcher duties the spec assigns to clients -${PLUGIN_ROOT}/${PLUGIN_DATA}expansion, env injection, command resolution - at install time, since the harness (not mdm) launches the servers. - Author tooling:
mdm plugins initscaffolds a conformant plugin;mdm plugins validatechecks one against the spec.
Non-goals (v1)¶
- Global scope. MCP wiring targets project config files; Claude Code's
global MCP config lives inside
~/.claude.jsonalongside unrelated state. - Well-known registry sources for plugins.
- Extension-directory semantics. mdm ignores reverse-domain extension dirs, as the spec instructs for unimplemented namespaces.
- An umbrella
mdm installthat restores skills + knowledge + plugins.
Design¶
The experimental gate¶
Identical to knowledge: experimental.Plugins, hidden from --help until
enabled, PersistentPreRunE refusal with an actionable message, stderr banner
on every invocation.
Command surface¶
mdm plugins
├── add <source> # Install into .agents/plugins/, link skills, wire MCP
├── remove [plugins...] # Unwire MCP, unlink skills, delete; --purge-data
├── list # Installed plugins from plugins-lock.json
├── update [plugins...] # Re-fetch from recorded source+ref; preserves data dir
├── validate [path] # Spec-conformance report (--json)
├── init [name] # Scaffold plugin.json + example skill; --with-mcp
└── install # Restore everything from plugins-lock.json (CI)
On-disk layout¶
.agents/plugins/<name>/- the plugin, copied verbatim. This directory is the spec'sPLUGIN_ROOT..agents/plugins-data/<name>/- the spec'sPLUGIN_DATA: created on install, preserved across updates, deleted only byremove --purge-data. Doctor suggests gitignoringplugins-data/once it holds anything..agents/skills/<skill>- a symlink into the plugin directory, and each harness's skills dir links to the canonical entry as usual. One copy on disk, atomic updates, and ownership is self-evident from the link target.
Lock file: separate by design¶
plugins-lock.json (project scope), for the same reason as
knowledge-lock.json: the skill locks are read into fixed structs and
rewritten wholesale, so an older mdm binary touching skills would silently
drop unknown keys. Entries record source/ref, install and data dirs, spec and
plugin versions, content hash, installed skills and harnesses, and the namespaced
MCP server ids written per harness.
Coexistence with skills-lock.json¶
plugins-lock.json exclusively owns plugin-delivered skills; the stable
skills commands recognize them but never manage them. Nothing plugin-related
is ever written to skills-lock.json. When the gate is on:
mdm skills listlabels plugin skills "(from plugin X)"mdm skills remove/ a standalone install over a plugin-owned skill refuses and points atmdm pluginsmdm skills update <name>on a plugin-owned skill warns and points atmdm plugins update <plugin>(plugin skills are never in the skills lock, so the filter would otherwise silently match nothing)- name collisions are first-come-first-served with a warning, in both directions
mdm skills installprints a one-line hint when aplugins-lock.jsonexists
MCP wiring (internal/mcpwire)¶
A per-harness target registry, deliberately separate from the stable
internal/harness registry: {HarnessName, ConfigPath, ServersKey, style}.
Claude Code (.mcp.json, typed entries with streamable HTTP spelled http)
and Cursor (.cursor/mcp.json, bare entries) ship in v1; another harness is one
map entry.
Server ids are namespaced <plugin>--<server> - the spec forbids -- inside
plugin names, so the split is unambiguous, and it avoids the : and __
sequences harnesses use for MCP tool-name mangling.
Because mdm writes config rather than launching servers, everything the spec
requires of the launcher is baked in at install time: ${PLUGIN_ROOT} and
${PLUGIN_DATA} become absolute paths, both variables are injected into the
server's env, and ./-prefixed commands resolve inside the plugin root (with
containment re-checked).
cwd is written only for a harness that reads one. Claude Code does not: its
stdio schema is command, args and env, and a configured cwd is dropped
rather than applied, so writing one put this machine's absolute path into a
committed file and changed nothing. A plugin that declares a cwd for such a
harness is told its server will start in the project root.
The written config is machine-local. The absolute paths above cannot be
made portable: Claude Code expands ${VAR} in .mcp.json only from variables
it already holds, and CLAUDE_PROJECT_DIR is set in the server's
environment rather than its own, so ${CLAUDE_PROJECT_DIR} in the config reads
as a missing variable; Cursor expands nothing. A stdio server living inside the
repository therefore has to be named by an absolute path. Treat the MCP config
the way .agents/ is already treated - generated output regenerated from
mdm.lock by mdm plugins install - rather than a file whose contents travel
between machines. mdm doctor says so once a plugin has wired servers into
it.
Config merges preserve every key mdm does not own; removal deletes exactly the
recorded ids and never the file.
Package boundary¶
Per the spec's package-boundary rule, every file mdm reads out of a plugin
must resolve inside the filesystem-resolved plugin root. An escaping
plugin.json symlink rejects the plugin, an escaping mcp.json disables
MCP, an escaping SKILL.md skips that skill, and the install copy skips
(with a warning) any symlink that resolves outside the root - so a
malicious plugin cannot pull files from elsewhere on disk into the
project. Directory symlinks are never followed during the copy; file
symlinks that resolve within the root are copied as regular files.
Security scan¶
Every selected plugin runs the mandatory hidden-character scan
(internal/security/markdownscan) over its whole directory before install,
exactly like skills and knowledge bundles. --allow-hidden-chars overrides
with a warning.
Doctor integration¶
Gated section: missing/invalid plugin dirs, content-hash drift, broken or
re-owned skill links, MCP ids missing from harness config, orphaned mdm-managed
ids, and the plugins-data/ gitignore hint.
Package layout¶
| Path | Role |
|---|---|
internal/plugin/ |
Spec conformance: manifest, name rules, mcp.json, path containment, discovery, hashing |
internal/mcpwire/ |
Per-harness MCP config targets, rendering, read-merge-write |
internal/lock/plugins.go |
plugins-lock.json read/write |
commands/plugins*.go |
The command group, one file per subcommand |
Testing strategy¶
- Unit (
internal/plugin): table-driven spec conformance - every name rule, the fatal/non-fatal manifest matrix, the closed server union, command/cwd/URL constraints, symlink-escape rejection, non-recursive placeholder expansion. - Unit (
internal/mcpwire): merge preserves foreign entries and unknown top-level keys, removal deletes only owned ids, render bakes absolute paths and injects env. - CLI (
tests/plugins_test.go): gate on/off, init→validate round trip, add→list→remove→update→install round trips, hidden-char blocking, dry run,--skip-mcp, broken-mcp resilience, lock isolation, ownership refusals, collision skips, doctor gating.
Graduation criteria¶
- The upstream spec sees real multi-client adoption without breaking changes.
- Global scope lands with a safe answer for shared global config files.
- MCP targets cover the majority of MCP-capable harnesses in
AllHarnesses. - Command surface survives a release cycle without changes.
Exit criteria (removal)¶
If the standard stalls or is superseded, remove the gate and command group in
a minor release; plugins-lock.json and installed plugin directories are
inert files a user can delete.