agent-plugins
A Rust implementation of the Agent Plugins specification v1.0.0 — the open, vendor-neutral format for packaging Agent Skills and MCP servers into distributable plugins.
Sans-IO at its core, so a plugin can be loaded from a directory, an archive, an object store, or an async runtime without the library knowing the difference. The TypeScript sibling lives at agent-plugin-ts and runs the same conformance corpus.
[]
= "0.1"
Loading a plugin
use ;
let plugin = load_dir?;
println!;
for skill in &plugin.skills
if let Configured = &plugin.mcp
// The spec says "SHOULD report"; this is where those reports arrive.
for note in &plugin.diagnostics
# Ok::
Design
Parse, don't validate. Every constraint the specification states gets a
type that carries its proof. A PluginName has satisfied §5.5. A
RelativePath has satisfied §4.1's spelling and containment rules. An
McpUrl has satisfied §7.2.1 — absolute, no user information, no fragment,
plain http only toward loopback. A Cwd can only be one of the three
permitted working-directory forms, so "some other string" is never a state the
rest of your code has to handle.
Failure boundaries are return types. The specification is unusually precise about how far a failure spreads, and the API mirrors it exactly:
| Failure | Boundary | Type |
|---|---|---|
| Invalid manifest | the whole plugin (§5.2) | Rejection |
| Unknown top-level manifest field | reported, ignored (§5.2) | Diagnostic |
Bad mcp.json |
MCP only (§7.2.2) | McpOutcome::Disabled |
| Bad server entry | that server (§7.2.2) | Diagnostic |
| Bad skill | that skill (§7.1) | Diagnostic |
Nothing fails silently, and nothing fails wider than the spec permits.
Sans-IO. Loading is a state machine that asks Questions — what kind
of entry is plugin.json? does skills/ resolve inside the root? read it;
list it — and consumes Answers from wherever the plugin actually lives:
let mut loading = start;
while let Some = loading.question
let plugin = loading.finish?;
For synchronous sources, implement Source and call load. For directories
and in-memory packages, load_dir and MemSource are built in.
Skill bodies are documents, not strings
Agent Skills load progressively: metadata at startup, the SKILL.md body on
activation, referenced files only when needed. This crate takes that one step
further — a skill's body arrives as a Document: a preamble plus a tree of
sections, each with a slug, a span, its own prose, and its full content
including subsections. Hand a model one section of a long skill instead of the
whole file, and follow the in-package links to the resources it points at.
let doc = &skill.body; // a Document, not a String
for in doc.outline
let rollback = doc.section.unwrap;
model.send; // just that subtree
for link in rollback.links
The parser handles the two things that actually bite in skill bodies: fenced
code is inert, so a # comment in a shell snippet is not a phantom heading,
and section spans follow heading level, ending exactly where a sibling or
shallower heading begins. Setext headings, skipped levels, and duplicate
headings (numbered slugs, as anchors do) all behave.
Launching a stdio MCP server
LaunchPlan resolves every placeholder and sequences the §9.1 environment
contract — configured overlay first, then PLUGIN_ROOT and PLUGIN_DATA
replacing any same-name entry:
let anchors = new;
if let Stdio = &entry.server
Conformance
This crate covers the client checklist in Appendix A of the specification:
$schema-selected offline validation, the closed manifest schema with its
exact fatal/non-fatal split, fixed-location component discovery, path
containment, the closed MCP transport union, per-boundary failure isolation,
and ${PLUGIN_ROOT}/${PLUGIN_DATA} expansion.
fixtures/cases/ pins the interesting boundaries — an invalid mcp.json that
must not stop skills from loading, thirteen individually invalid server
entries beside one good one, a skills that is a file, an mcp.json that is
a directory, plugins from a future spec version. Each case is a plugin package
plus an expected.json. The corpus is mirrored in agent-plugin-ts; when
you change one, sync the other with just sync-fixtures so the two
implementations keep agreeing case-by-case.
Development
just drives everything:
Releasing
Releases are tag-driven. cargo release bumps the version, rewrites the
changelog, commits, tags, and pushes; the tag triggers CI, which re-runs every
check on a clean runner and only then publishes to crates.io.
A release needs the CARGO_REGISTRY_TOKEN secret set on the crates-io
environment. See docs/RELEASING.md for the one-time
setup, including how to switch to crates.io Trusted Publishing instead of a
long-lived token.