@jini-ai/agent-plugins
v0.4.2
Published
Support for Agent Plugins, the open, vendor-neutral spec (v1.0.0, published 2026-08-06, Technical Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel) for bundling Agent Skills and MCP servers into one portable d
Readme
@jini-ai/agent-plugins
Leon, this is you from the past. Do not try to merge
pluginsandagent-plugins.
Support for Agent Plugins, an open, vendor-neutral spec (v1.0.0, published 2026-08-06) for bundling Agent Skills and MCP servers into one portable directory. Published by a Technical Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel. Source: https://developers.googleblog.com/agent-plugins-package-your-skills-tools-and-more/
If you are an agent reading this cold: this document is written so you don't need the source
article to understand the shape of a plugin or what this package provides. PluginManifest and
McpManifest (exported from the package root) type every field confirmed against the published
schemas (AGENT_PLUGINS_SCHEMA_URL, AGENT_PLUGINS_MCP_SCHEMA_URL), not inferred — both still
carry an index signature in case a future schema revision adds a field neither type has caught up
to yet.
Extracted 2026-08-18 from @jini-ai/plugins, where this content used to live nested under a
./agent-plugins subpath. That package now only reserves the unrelated, unimplemented ./host
format (Jini's own host-extension plugin system — a different concept entirely, see "Not this
package" below) — everything Agent-Plugins-shaped moved here, one level up, since there was no
longer a second format sharing the namespace to disambiguate against.
The problem this solves
Before this spec, shipping the same skill or MCP server to multiple agent clients meant maintaining incompatible copies — every client used a different manifest format and directory layout ("fork and drift"). Agent Plugins standardizes the packaging format only; each client still decides its own install mechanism, permission model, and sandboxing.
What a plugin looks like on disk
A plugin is a directory. Example (this package's own bundled plugin, ui-ux-design/):
ui-ux-design/
├── plugin.json # manifest — minimally { "$schema": ..., "name": "ui-ux-design" }
├── mcp.json # MCP server declarations (empty here — example only)
├── skills/
│ └── <skill-name>/
│ ├── SKILL.md # Agent Skills format: frontmatter (name, description) + body
│ ├── references/ # optional supporting docs the skill body links out to
│ ├── examples/ # optional
│ └── scripts/ # optional
└── com.anthropic.claude-code/ # client extension directory (§8.2) — example only
└── hooks/
└── hooks.jsonplugin.json optional fields beyond $schema/name: version, description, author
({name, email, url}), homepage, repository, license, keywords (string array), and
extensions — an object keyed by reverse-domain client namespace (e.g. com.example.client) for
client-specific data the schema assigns no semantics to.
mcp.json declares MCP servers under mcpServers, one of three explicit transport shapes — no
guessing which one a client should assume:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"local-validator": {
"type": "stdio",
"command": "./bin/validator",
"args": ["--data", "${PLUGIN_DATA}/validator"],
"env": { "CONFIG": "${PLUGIN_ROOT}/config.json" },
"cwd": "${PLUGIN_ROOT}"
},
"deployment-api": {
"type": "streamable-http",
"url": "https://deploy.example.com/mcp",
"headers": { "X-Tenant": "public-tenant" }
},
"legacy-events": {
"type": "sse",
"url": "https://legacy.example.com/sse"
}
}
}ui-ux-design/mcp.json ships with an empty mcpServers: {} — a valid, working
example of the file's shape, not a real server declaration. Replace it (or delete it — mcp.json
is optional) the moment this plugin actually needs one.
A plugin can also have reverse-domain-namespaced directories at its root — §8.2 of the
specification: "the extension directory for a namespace is the top-level directory named after
it," contents entirely client-defined, and clients that don't recognize a namespace simply ignore
it. The spec's own example is com.example.client/hooks/hooks.json — using example.com the same
way an RFC does, since the spec assigns no real namespace to any actual client and publishes no
namespace registry. ui-ux-design/com.anthropic.claude-code/hooks/hooks.json
follows the same shape with a real client substituted in: com.anthropic.claude-code is a
plausible reverse-DNS guess (Anthropic controls anthropic.com; Claude Code is the client this
whole package was built inside), not a confirmed or registered identifier — nothing publishes
one. Content is illustrative only, per the same "no portable semantics" rule.
Discovery (how a client finds/installs a plugin in the first place) is explicitly out of scope for the packaging spec itself — it lives in separate layers (Agentic Resource Discovery, an AI Catalog format) that this package does not implement.
What's in this package
| export | runtime | contents |
|---|---|---|
| . (root) | universal | PluginManifest/McpManifest types, validatePluginManifest({ value })/validateMcpManifest({ value }) structural validators (plus compatible isPluginManifest(value)/isMcpManifest(value) wrappers), and the known path constants (PLUGIN_MANIFEST_FILENAME, PLUGIN_SKILLS_DIRNAME, PLUGIN_MCP_MANIFEST_FILENAME). No filesystem access, no DOM. |
| ./manifest | universal | Strict manifest/MCP parsing and typed namespace readers; object structural validators are also re-exported here. |
| ./lifecycle | node | Host-injected installation, activation, locks, digests, bundles, trusted files, reference resolution, ranking and MCP provisioning helpers. |
| ./lifecycle/node | node | createNodeAgentPluginEffects({}) opt-in native effects. |
| ./lifecycle/yauzl | node | createYauzlAgentPluginArchiveReader({ yauzl }); the host supplies the optional peer library. |
| ./ui-ux-design/* | — | Raw files of the bundled ui-ux-design plugin — static JSON/Markdown, not run through the TS build. |
The universal root handles structural validation. The separate Node lifecycle entry implements local archive installation and host-owned activation policy; it does not implement remote catalog discovery.
Not this package: @jini-ai/plugins (./host)
@jini-ai/plugins is a different, sibling package — Jini's own host-extension plugin format
(manifest + setup() + hooks + activation), unrelated to the third-party Agent Plugins spec this
package supports. Do not confuse the two — "agent plugins" (this package, a
public spec) and "Jini plugins" (@jini-ai/plugins's ./host, Jini's own unbuilt format) share
the word "plugin" and nothing else.
Bundled plugins
Real, install-ready plugin directories shipped alongside the code — not illustrative examples.
Each lives at the package root under its own name (ui-ux-design/).
Plugins live at the package root rather than inside src/ because src/ is the TypeScript
compile root (rootDir: "src", include: ["src"]) — a plugin's skill trees can carry files
(.tsx examples, etc.) that are illustrative content, not package code, and putting them under
src/ makes tsc try to compile them (measured, on ui-ux-design: 207 errors from the three
shadcn-ui/examples/*.tsx files alone). Static content and compiler input are kept in separate
trees on purpose.
ui-ux-design/
Bundles the AI-Dev-Shop Web Design agent's full skill set (per its agents/web-design/skills.md
persona and the framework/routing/skills-registry.md ownership mapping) into one portable plugin:
ui-ux-design— design-foundations (tokens, typography, spacing, breakpoints, component state matrix) and first-impression polish, scanning hierarchy, conversion-focused visual signals — one skill, not two. AI-Dev-Shop merged its former separateux-designandpremium-uiskills into this singleui-ux-designskill; this sample tracks that merge instead of carrying the two predecessor skills as stale copies (see git history for the 2026-08-12 consolidation).interface-design— repeatable, memory-consistent visual systems for dashboards/admin panels/appsgstack-design— manual four-mode workflow (consultation, shotgun, html, review)frontend-accessibility— WCAG 2.1 AA checklistvercel-web-design-guidelines— Vercel Web Interface Guidelines auditorshadcn-ui— shadcn/ui (Radix + Tailwind) component discovery/integration guidanceweb-compliance— legal/compliance checkpoints for public-facing UX flows
Each skill directory is a verbatim copy of the corresponding AI-Dev-Shop/skills/<name>/ tree —
copied rather than referenced, because the whole point of a plugin is that it is self-contained and
portable to a host that has never heard of AI-Dev-Shop. AI-Dev-Shop/agents/web-design/skills.md
itself (the persona that composes these skills into one role) is not bundled — it is ADS-specific
routing glue, not a portable skill.
Consumers may read bundled raw assets through the public wildcard subpath. Product-specific plugin assets are owned and shipped by their product, outside this library.
Scripts
pnpm --filter @jini-ai/agent-plugins build
pnpm --filter @jini-ai/agent-plugins typecheck
pnpm --filter @jini-ai/agent-plugins testThe build and typecheck include lifecycle test sources. Copied Node test options are translated to Vitest registration, preserving per-test timeouts and platform skip conditions.
Adding another bundled plugin
- Create
<plugin-name>/plugin.jsonat the package root with at least{ "$schema": AGENT_PLUGINS_SCHEMA_URL, "name": "<plugin-name>" }. Do not put it undersrc/— see "Bundled plugins" above for why. - Add
<plugin-name>/skills/<skill-name>/SKILL.mdper skill (plus anyreferences/,examples/,scripts/the skill needs). - Register it in
package.json: anexportskey"./<plugin-name>/*": "./<plugin-name>/*", a matchingjini.entrieskey (the R8 guard requires every export to have one), and afilesentry. - Add coverage in
src/__tests__/manifest.test.tsfollowing theui-ux-design pluginblock — assert the manifest validates and the expected skill directories exist.
Lifecycle API and ports
New public callables use (requiredArgs, optionalArgs), with effects supplied as ports.
Create and retain one lifecycle per host context:
import { createAgentPluginLifecycle, createAgentPluginLayout } from '@jini-ai/agent-plugins/lifecycle';
import { createNodeAgentPluginEffects } from '@jini-ai/agent-plugins/lifecycle/node';
const layout = createAgentPluginLayout({ root: pluginRoot });
const lifecycle = createAgentPluginLifecycle({
...createNodeAgentPluginEffects({}),
layout,
productName,
extensionNamespace,
bundledArchiveMagic,
deliveryMode,
seededEnabledPluginIds,
retiredBundledPlugins,
formatPluginToolPointer,
mcpProvisioning,
outboundGuard,
fetch: ({ url }, options) => fetch(url, options),
}, { onEvent, readServerMetadata });
const downloaded = await lifecycle.fetchAgentPluginArchive({ url }, { signal });
const installed = await lifecycle.installAgentPlugin({
archive: downloaded.archive,
expectedSha256: downloaded.sha256,
archiveReader,
layout,
workspaceId,
});All names in the example are supplied by the host. There is no default storage root,
namespace, product name, bundle policy or archive framing. Preserve your existing framing
when adopting the package. Prefer pinned hashes for remote installations; trust-on-first-use
is an explicit choice in installAgentPluginFromUrl.
Filesystem effects use native node:fs/promises signatures and native FileHandles. Inject
instrumented native effects directly through filesystem; createNodeAgentPluginEffects({})
provides the default. Clocks extend core Clock with monotonicMs() and sleep({ ms });
IDs extend core IdGenerator with random().
Generic locks are imported directly from @jini-ai/platform/fs/file-lock. Lifecycle activation
uses withFileLock({ lockPath, run }, { timeoutMs: 15000, staleMs: 10000, pollMs: 10, ...effects }).
Lock helpers, errors and constants are no longer members/exports of the lifecycle API.
Archive readers implement entries({ archive }); file entries expose openReadStream({}).
createYauzlAgentPluginArchiveReader({ yauzl }) accepts the native library protocol explicitly;
no peer library is imported by the core. Fetch ports implement fetch({ url }, requestOptions).
The outbound guard runs before every request and redirect. The host's fetch adapter must
pin DNS if its security policy requires protection against DNS rebinding.
Public callbacks also receive objects: pluginIdOf({ item }), run({ lock }),
isProcessAlive({ pid }), onStaleLockRemoved({ holder }), onInactive({ plugin })
and now({}). Lock ownership checks use lock.assertHeld({}). Constructors receive
named fields, for example new AgentPluginInstallError({ code, message }, { cause }).
recordBundledAgentPluginDigests accepts the clock override in its second object;
findTrustedPluginPackages accepts orderByPluginId and onInactive in its second object.
parseAgentPluginMcpConfig({ value, extensionNamespace }, { pluginManifest, readServerMetadata })
accepts optional namespace metadata separately. Strict parsing preserves standard object
shaped authors and older string authors. Inline transport fields never become reviewed-read
metadata. MCP provisioning helpers delegate to the host and notify only after success;
filesystem installation and federation remain separate operations with host-owned recovery.
The legacy root validators keep their positional shape and behavior. New code can use the
object-shaped validatePluginManifest and validateMcpManifest facades. Internal composition
factories are implementation details and are not package exports.
See integration-extraction.md for the reconciliation, source mapping, rewire ledger and verification commands. All verification is deferred by owner directive.
For a host that needs only activation, use createAgentPluginActivations with
createNodeAgentPluginEffects({}) instead of supplying unused network/provisioning
ports. Retain one returned object for the process lifetime. See API.md for the
object-shaped reads, filters and optional legacy metadata reader.
Persistent plugin state (Layout B)
@jini-ai/agent-plugins/persistent-state is a build-free Node entry with declarations.
pluginStatePaths({ workspaceRoot, pluginId }) gives one plugin's package/sha256/,
memory/learned/, memory/notes/ and data/ paths. createPluginMemory injects the
filesystem, the package containment primitive and a cross-process lock. Its plugin-facing
learned capability cannot select another id or write user notes. Defaults: 1 MiB learned,
16 KiB notes, 128 files per tier; callers may lower these caps.
migratePluginLayout moves legacy packages, memory and PLUGIN_DATA, preserving conflicts
and malformed installs for human repair. It records version 2 only after success and repairs
frozen package roots after interrupted moves. Lock files use workspace staging scratch.
Uninstall keeps persistent state by default; deleting it requires a matching confirmed preview.
The reserved workspace directory ids staging, packages, memory, and data are refused
as plugin ids so package state cannot overlap migration inputs or installation scratch.
The dev.tovu.memory proposal is consumer policy and is not implemented by this neutral entry.
No version bump or publishing accompanies this workspace implementation.
