Tutorial
How Agent Plugins give an AI coding agent new capabilities: Plugin Manifest, Skills, MCP, and the JSON you hold after install
Installing a plugin does not make the model smarter. The client just reads a few more JSON files from fixed paths.
The last two posts covered what the box looks like and when not to pack one. Today we assume you already chose a Plugin. The question readers actually ask: after Cursor, Claude Code, or Antigravity installs that directory, how does the model suddenly query invoices and write the weekly summary? It is not a weight update and it is not a rewritten system prompt. Agent Plugins 1.0.0 is blunt: read root plugin.json first, then discover skills/ and mcp.json at fixed locations. The Google Cloud Developer Plugin follows the same walk. This piece finishes the JSON hops after install and hands off to MCP and JSON Schema.
Five hops, not “the model learned it”
“The agent automatically gained a capability” sounds like the model learned a skill. In engineering it is five hops, and you cannot skip one. Hop one: the client roots the directory; a resolved path that escapes the plugin root is denied, including a symlink that points outside. Hop two: read plugin.json, validate the closed schema, take name and the spec version. Fail this hop and the whole package is rejected — hops three through five never run. Hop three: if skills/ exists, look only at immediate children that contain a regular file named exactly SKILL.md, and inject name plus description. Hop four: if mcp.json exists, connect using each entry’s type, then tools/list after the handshake. Hop five: the model matches a description or a tool name, then reads the skill body or issues tools/call.
The spec deliberately ignores what the install button looks like. The Google Developers Blog says it in the open: install, permissions, sandbox, and confirmation UX are each client’s job. Agents CLI, Cursor, and Claude Code may show three different dialogs. What travels is the directory and two closed JSON files. Review should not ask “where does the user click?” It should ask “which objects are now in memory?” No manifest, no later hops. Manifest ok but mcp.json $schema disagrees with plugin.json: disable MCP, keep skills. One SKILL.md that fails Agent Skills: skip that skill only.
Horizontal delegation is still not this pipeline. How another team’s invoice agent is found is an Agent Card, covered in the A2A post. Today we only ask how this coding agent grows a set of skills and tools. Call “new capability” a discovery result, then inspect the JSON at each hop.
| This hop | JSON the client now holds | What the model can do |
|---|---|---|
Read plugin.json | Identity: name / version / $schema | Nothing yet — the box is merely valid |
Walk skills/ | Skill metadata array (no body yet) | Can pick a brief; body loads on demand |
Connect MCP, then tools/list | Tool names plus inputSchema | Can fill arguments; nothing has run |
Manifest first: plugin.json is the identity contract
The client MUST read root plugin.json before it discovers components. You cannot rename the file, and you cannot inline skills or MCP into the manifest. The schema is closed: only $schema, name, version, description, author, homepage, repository, license, keywords, and extensions. Extra top-level keys MUST be reported and ignored — they are not a reason to reject the plugin. Fatal errors are missing required fields, wrong types, or an illegal name: reject the package and discover nothing. For 1.0.0, $schema MUST be https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. The client uses it to pick local rules and MUST NOT fetch a schema while loading.
name is an identifier, not a store title. Length 1–64; only a-z, digits, hyphen, and period; first and last characters alphanumeric; no -- or ... My-Plugin and -start reject the whole package. SemVer is recommended for version, but “does not look like SemVer” is not a reject reason. The author object may only contain name / email / url. Client-private data goes under extensions.com.example.client or a reverse-domain directory at the root. Do not invent a fifth top-level key for hooks. If you put hooks at the top of plugin.json, the spec says ignore them — your IDE may “happen to read” them, the next client will not.
Below is a full manifest you can commit: more than the two-field minimum, with metadata for humans. Make it parse and pass the official schema before you argue about the install button. keywords help a catalog. description helps a person decide to install. It does not help the model pick a tool. Skills are chosen from the SKILL.md description; tools are chosen from tools/list. Writing a tool brief into the manifest still leaves the discovery surface empty.
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "invoice-ops",
"version": "1.2.0",
"description": "Invoice query MCP plus the weekly-summary skill, shipped as one directory",
"author": {
"name": "Finance Platform",
"url": "https://docs.example.com/invoice-ops"
},
"homepage": "https://docs.example.com/invoice-ops",
"repository": "https://github.com/example/invoice-ops",
"license": "MIT",
"keywords": ["invoices", "weekly-summary", "mcp"]
}
After install: the capability snapshot in the client
The spec does not require clients to export an “installed capabilities” file. Review still needs to see what sits in memory after install. Fold the five hops into one snapshot JSON: plugin identity, discovered skills (metadata only), MCP connection state, and the tool contracts from tools/list. That snapshot is not an instance of plugin.schema.json — do not validate it with the package schema. It is a CI fixture: assert skill count, tool names, and required fields on inputSchema. If install succeeds and the snapshot does not match, discovery or the handshake broke. The model did not “fail to learn.”
“Automatically gained” is visible in this object. skills[].loaded is metadata, not body — about a hundred tokens at startup, body on demand. tools[] comes from tools/list after the handshake, not from a handwritten list in plugin.json. mcpServers[].status is runtime, not the package contract. Empty tools with connected means the handshake worked and the server exposed nothing — debug the server, not the manifest. The inverse, a valid manifest and zero skills in the snapshot, usually means SKILL.md was nested one directory too deep.
After the Google Cloud Developer Plugin installs, the client side is the same shape: box identity, metadata for the gcloud guardrail skill, and the Developer Knowledge MCP tool list. Users say “the agent can search Cloud docs now.” In data, tools grew a few entries. Diff the snapshot against committed plugin.json / mcp.json and you will catch anyone writing runtime state back into the package files. That is drift, not a spec field.
{
"plugin": {
"name": "invoice-ops",
"version": "1.2.0",
"spec": "1.0.0"
},
"skills": [
{
"name": "write-weekly-summary",
"description": "Turn invoice query results into the weekly summary finance reads. Use when the user asks for a week-end report.",
"path": "skills/write-weekly-summary/SKILL.md",
"loaded": "metadata"
}
],
"mcpServers": [
{
"id": "invoice-tools",
"type": "streamable-http",
"status": "connected"
}
],
"tools": [
{
"name": "query_invoices",
"server": "invoice-tools",
"inputSchema": {
"type": "object",
"required": ["week"],
"properties": {
"week": { "type": "string", "pattern": "^[0-9]{4}-W[0-9]{2}$" },
"status": { "type": "string", "enum": ["open", "paid", "overdue"] }
}
}
}
]
}
The skill hop: frontmatter becomes the discovery surface
Agent Plugins does not rewrite SKILL.md. Discovery is one rule: an immediate child of skills/ that contains a regular file named exactly SKILL.md. Hide one at skills/deploy/extra/SKILL.md and it is invisible. A skill that fails Agent Skills MUST be skipped; other skills and MCP keep loading. What enters context is frontmatter name and description, plus a path so the body, scripts/, and references/ can load later.
The description must say what it does and when to use it. Call it invoice-ops-skill-v2 or a first-person slogan and the discovery surface is zero — the box installed, the model never picks it, and users blame the plugin. The brief is what broke. scripts/ still means “run this with the shell you already have”; arguments are argv, not first-class tools from tools/list. Do not put script names in the snapshot’s tools[]. If they appear, someone filed a skill attachment as MCP.
Loading the body on demand saves context. Keeping loaded: metadata in the snapshot catches the regression of stuffing a whole runbook into the system prompt. A window blown up by a brief is a client loading-policy bug, not a Plugin format bug. The spec only guarantees the skill can be found. How it is shown to the model and the user stays client-defined.
The MCP hop: connect, then tools/list
mcp.json MUST sit at the root. It MUST NOT be inlined into plugin.json and MUST NOT live on an alternate core path. Top level allows only $schema and mcpServers. Pin $schema to https://agent-plugins.org/schemas/1.0.0/mcp.schema.json, and it MUST match the spec version declared in the manifest. A mismatch disables MCP for that plugin only. Every server MUST set type explicitly: stdio, streamable-http, or optional legacy sse. Clients MUST NOT infer transport from object shape. A streamable-http url MUST be an absolute http/https URL; non-loopback MUST be https. headers are visible package data, not a secret slot.
The discovery surface after connect is tools/list. The package file answers where to connect. The tool contract answers whether this hop’s arguments are legal. Do not copy inputSchema into plugin.json, and do not copy name / version into inputSchema. An auth failure is a connection failure for that server, not illegal plugin config — the spec defines no portable OAuth fields; credentials stay in the client runtime. Wire details live in What is MCP.
Below is a portable remote MCP fragment. Keep API keys out of repo fixtures. After connect, pour tools/list into the snapshot’s tools[]. A handshake failure skips that one server; other servers and skills continue. That failure boundary is in the spec, not a product slogan.
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"invoice-tools": {
"type": "streamable-http",
"url": "https://billing.example.com/mcp"
}
}
}
| Failure | What dies | What stays up |
|---|---|---|
Uppercase in plugin.json name | The whole package; no components discovered | Nothing |
mcp.json $schema disagrees with the manifest | All MCP for that plugin | Skills keep loading |
One SKILL.md has broken frontmatter | That one skill | Other skills + MCP |
Independent failure, fixtures in JSONVue
Keep at least four review fixtures: the legal plugin.json above, the capability snapshot, a portable mcp.json, and one query_invoices arguments object. The first MUST pass official plugin.schema.json. The second uses your own snapshot schema, or structural asserts — do not force the package schema onto it. The third passes mcp.schema.json. The fourth passes the tool inputSchema. Four JSON files, four jobs: identity, inventory, connection, call.
Add two negatives: set name to Invoice-Ops; drop type on one mcp.json entry. The first MUST reject the package. The second skips only that server. If CI treats an unknown top-level field as fatal, you are stricter than the client — the spec says report, ignore, and keep loading. No secret belongs in a committed fixture.
You can do this in the browser:JSON formatterto see whether the manifest, snapshot, and mcp.json parse;JSON Schema validatorto check $schema, name, mcpServers, and inputSchema;JSON Diffto catch runtime state written back into package files. Nothing leaves the machine. Further reading:MCP and JSON Schema, the Plugins overview, and when to pack a box.
Related: Google Agent Plugins 2026, Skills vs MCP vs Plugins, MCP and JSON Schema, What is MCP.
FAQ
Does installing a Plugin fine-tune the model?
No. Weights do not change. The client gained an identity object, a set of skill metadata, and tool contracts from tools/list. It “can” because the discovery surface changed, not because the model learned invoicing.
Can I put the tool list in plugin.json and skip mcp.json?
No. The manifest cannot inline components or change discovery paths. The tool list arrives from tools/list after the handshake. A top-level list in plugin.json is ignored. A list under extensions is meaningful to one client and disappears on the next.
Can I validate the snapshot with official plugin.schema.json?
No. The official schema describes box identity. The snapshot is client-assembled inventory: runtime status and tool inputSchema. Write a snapshot schema, or assert the fields you care about in CI.
Install UX differs per client. Is the Plugin still portable?
The package is portable. Install does not have to be. The spec excludes install, permissions, and sandboxing on purpose. Switch clients and the directory plus two closed JSON files stay the same. Confirmation dialogs and enterprise policy may not.
Takeaways and next steps
In 2026, “a Plugin lets a coding agent gain capabilities automatically” collapses to one line: a new capability is a discovery result, not a weight. Finish five hops and memory holds identity, skill metadata, and tool contracts. Skip a hop and users report “installed, still can’t.”
Ship in this order: pass official schema on plugin.json; walk skills/ and mcp.json; freeze a snapshot fixture; validate the first tools/call against inputSchema. Keep the four contracts in JSONVue. Read the Plugins overview for the box, the decision post for whether to pack, and the MCP posts for the wire.