Agent Plugins 1.0 Build Pipeline: Package Skills + MCP Servers
Package AI agent capabilities into portable, vendor-neutral Agent Plugins that bundle Skills with MCP servers, then automate schema validation, multi-client smoke tests, and catalog publishing.
Deepak Bagada
CEO, SaaSNext
- Agent Plugins 1.0.0 (Aug 6, 2026) is a vendor-neutral, CC-BY-4.0 spec stewarded by the Agentic AI Foundation with Core Maintainers from Amazon, Cursor, Microsoft, OpenAI, Vercel, and Google.
- A plugin is one portable directory: plugin.json manifest, skills/ subdirectory, and mcp.json with explicit transport types.
- Automate the lifecycle with PydanticAI-typed tools and a LangGraph state machine that validates, smoke-tests, and publishes.
- Publish with MIME type application/agent-plugins+json and register the resource with Google's Agentic Resource Discovery (ARD).
- Skills Over MCP and the stateless MCP 2026-07-28 core make plugin runtime behavior predictable across hosts.
The Problem: Portable Agent Capabilities
On August 6, 2026, the Agentic AI Foundation (AAIF) under the Linux Foundation formally published Agent Plugins 1.0.0, a vendor-neutral, CC-BY-4.0-licensed specification written by a Technical Steering Committee of Core Maintainers from Amazon, Cursor, Microsoft, OpenAI, and Vercel, with Google joining as a Core Maintainer. The spec answers one painful question for every team shipping agentic software: how do you move a working agent, its tools, its skills, and its MCP connections from one client to another without rewriting everything?
Before Agent Plugins, every host reinvented packaging. VS Code had its own extension format, Cursor had prompts and tools, ChatGPT had GPTs, Copilot had agents and skills, and Codex had its own conventions. A skill that worked beautifully in Cursor was useless in ChatGPT, and an MCP server configured by hand in one product had to be re-configured by hand everywhere else. Distribution of agent capability was a walled-garden mess.
Agent Plugins 1.0.0 changes the contract. A plugin is a single portable directory that captures three things:
- a top-level
plugin.jsonmanifest declaring name, version, schema, and entry points; - a
skills/subdirectory with one subdirectory per skill in the Agent Skills format; - an
mcp.jsonfile declaring every MCP server the plugin needs, each with an explicit transport type:stdio,streamable-http, or legacyhttp+sse.
Because the format is plain files in a directory, plugins can be zipped, versioned in git, signed, scanned, and shipped through normal software distribution. Hosts that adopted the spec, including VS Code, Cursor, GitHub Copilot, ChatGPT, Codex, Kiro, AWS Agent Toolkit, and Google Data Agent Kit, can import the same plugin and expose the same capabilities. That solves distribution at the protocol level rather than per-vendor.
For builders this is a workflow opportunity. Packaging by hand is error-prone, validation is boring, and testing against eight clients is exhausting. This article walks through a production-grade build and distribution pipeline built with PydanticAI and LangGraph that scaffolds, validates, smoke-tests, and publishes Agent Plugins to a catalog. For the broader ecosystem of portable AI servers, check the Daily AI World MCP Directory and the AI Workflows library.
Anatomy of an Agent Plugin 1.0 Package
A valid 1.0.0 plugin directory looks like this:
my-agent/
├── plugin.json
├── skills/
│ ├── code-review/
│ │ ├── SKILL.md
│ │ └── references/
│ │ └── rubric.md
│ └── incident-triage/
│ ├── SKILL.md
│ └── scripts/
└── mcp.json
The plugin.json manifest carries the metadata and entry points:
{
"name": "org.acme.support-agent",
"version": "1.4.2",
"schema": "https://agentplugins.dev/schema/1.0.0/plugin.schema.json",
"description": "Production support agent with code-review and triage skills.",
"entrypoints": {
"skill": "skills/code-review/SKILL.md",
"mcp": "mcp.json"
},
"licenses": ["CC-BY-4.0"],
"authors": ["platform@acme.com"],
"client": {
"min_versions": {
"vscode": "1.92",
"cursor": "0.44",
"copilot": "1.24",
"chatgpt": "2026.07"
}
}
}
And mcp.json declares servers with their explicit transport, which matters enormously on the 2026 stateless MCP core:
{
"mcpServers": {
"jira": {
"transport": "streamable-http",
"url": "https://mcp.acme.dev/jira",
"headers": { "Authorization": "Bearer ${JIRA_TOKEN}" }
},
"postgres": {
"transport": "stdio",
"command": "uvx",
"args": ["mcp-postgres", "--conn", "${DATABASE_URL}"]
}
}
}
Notice the variables in ${...}. Plugins must be shareable without leaking secrets, so the pipeline substitutes variables at install time from a per-host secret store rather than baking credentials into the archive.
Architecture Diagram: The Build & Distribute Pipeline
graph TD
A[Git push: skills + mcp.json + plugin.json] --> B[Scaffold Node - PydanticAI]
B --> C[Manifest Validator]
C --> D{Semantic validation}
D -- fail --> E[Fix + Retry Loop]
E --> B
D -- pass --> F[Smoke Test Matrix]
F --> G[VS Code harness]
F --> H[Cursor harness]
F --> I[Copilot harness]
F --> J[ChatGPT + Kiro harness]
G --> K{Aggregate results}
H --> K
I --> K
J --> K
K -- any fail --> E
K -- all pass --> L[Sign + Package Node]
L --> M[Publish to AI Catalog]
M --> N[ARD index - agent-plugins resource]
M --> O[Registry metadata + MIME header]
The Pipeline: Scaffold, Validate, Smoke Test, Publish
The implementation uses PydanticAI for strictly typed tools and LangGraph for the orchestration graph. First, the environment:
# .env
AGENT_PLUGINS_SCHEMA_URL=https://agentplugins.dev/schema/1.0.0/plugin.schema.json
PLUGIN_ID=org.acme.support-agent
PLUGIN_VERSION=1.4.2
MCP_TRANSPORT=streamable-http
CATALOG_API=https://catalog.acme.dev/v1/plugins
CATALOG_TOKEN=${CATALOG_TOKEN}
ARD_INDEX_URL=https://ard.google.com/index
SMOKE_CLIENTS=vscode,cursor,copilot,chatgpt,kiro
SMOKE_TIMEOUT_SECONDS=300
MAX_MANIFEST_ATTEMPTS=3
RETRY_BASE_SECONDS=2
RETRY_MAX_SECONDS=30
RETRY_MULTIPLIER=2.0
OPENAI_API_KEY=${OPENAI_API_KEY}
Next, typed schemas that mirror the 1.0.0 manifest so validation is data-driven, not hand-written:
# schemas.py
from __future__ import annotations
from enum import Enum
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, Field, HttpUrl, model_validator
class TransportType(str, Enum):
STDIO = "stdio"
STREAMABLE_HTTP = "streamable-http"
HTTP_SSE = "http+sse" # legacy, deprecated on MCP 2026-07-28
class MCPServerConfig(BaseModel):
transport: TransportType
url: HttpUrl | None = None
command: str | None = None
args: list[str] = Field(default_factory=list)
headers: dict[str, str] = Field(default_factory=dict)
ttl_ms: int | None = None # SEP-2549 caching
cache_scope: Literal["none", "request", "connection"] = "none"
@model_validator(mode="after")
def _transport_consistency(self) -> MCPServerConfig:
if self.transport is TransportType.STDIO and not self.command:
raise ValueError("stdio transport requires a command")
if self.transport is TransportType.STREAMABLE_HTTP and not self.url:
raise ValueError("streamable-http transport requires a url")
return self
class SkillEntry(BaseModel):
name: str = Field(min_length=2, max_length=64)
path: str
description: str = Field(default="")
class ClientMinVersion(BaseModel):
vscode: str | None = None
cursor: str | None = None
copilot: str | None = None
chatgpt: str | None = None
kiro: str | None = None
class PluginManifest(BaseModel):
name: str = Field(pattern=r"^[a-z0-9]+\.[a-z0-9][a-z0-9.-]*$")
version: str = Field(pattern=r"^\d+\.\d+\.\d+$")
schema: HttpUrl
description: str = Field(default="")
entrypoints: dict[str, str]
licenses: list[str] = Field(default_factory=list)
authors: list[str] = Field(default_factory=list)
client: ClientMinVersion = Field(default_factory=ClientMinVersion)
@model_validator(mode="after")
def _has_skill_or_mcp(self) -> PluginManifest:
if "skill" not in self.entrypoints and "mcp" not in self.entrypoints:
raise ValueError("plugin must declare at least one skill or MCP entrypoint")
return self
The tools each encapsulate one pipeline stage. PydanticAI turns them into typed tools an orchestrator agent can call:
# tools.py
from __future__ import annotations
import json
import shutil
import subprocess
import zipfile
from pathlib import Path
from typing import Annotated
import httpx
from pydantic import TypeAdapter, ValidationError
from pydantic_ai import RunContext, Tool
from schemas import PluginManifest
PLUGIN_ROOT = Path("/opt/plugins")
def load_manifest(path: Path) -> PluginManifest:
return PluginManifest.model_validate_json((path / "plugin.json").read_text())
def validate_manifest(ctx: RunContext, plugin_id: str) -> str:
"""Validate plugin.json against the Pydantic schema mirror of the spec."""
pkg = PLUGIN_ROOT / plugin_id
try:
manifest = load_manifest(pkg)
for skill in (pkg / "skills").iterdir():
if not (skill / "SKILL.md").exists():
raise ValidationError.from_exception_data(
"SKILL.md", [{"type": "missing", "loc": (skill.name,), "input": str(skill)}]
)
mcp_file = pkg / "mcp.json"
if mcp_file.exists():
json.loads(mcp_file.read_text()) # basic JSON sanity
return f"manifest OK: {manifest.name}@{manifest.version}"
except (ValidationError, ValueError, OSError) as exc:
return f"manifest INVALID: {exc}"
def smoke_test(ctx: RunContext, plugin_id: str, client: str) -> str:
"""Run the client harness for one host (vscode, cursor, copilot, chatgpt, kiro)."""
pkg = PLUGIN_ROOT / plugin_id
harness = Path("/opt/harnesses") / f"run_{client}.sh"
if not harness.exists():
return f"{client}: no harness installed"
result = subprocess.run(
[str(harness), str(pkg)],
capture_output=True,
text=True,
timeout=300,
)
return f"{client}: {"PASS" if result.returncode == 0 else "FAIL"} - {result.stdout.strip()[-200:]}"
def publish_plugin(ctx: RunContext, plugin_id: str, archive: str) -> str:
"""Upload a signed .zip to the AI Catalog and register it with ARD."""
headers = {
"Authorization": f"Bearer {ctx.deps.catalog_token}",
"Content-Type": "application/agent-plugins+json",
"X-Plugin-Id": plugin_id,
}
with open(archive, "rb") as fh:
resp = httpx.post(ctx.deps.catalog_api, headers=headers, content=fh.read(), timeout=60)
if resp.status_code >= 400:
return f"publish FAILED {resp.status_code}: {resp.text[:200]}"
ard = httpx.post(
f"{ctx.deps.ard_url}/resources",
json={"type": "agent-plugins", "id": plugin_id, "mime": "application/agent-plugins+json"},
timeout=30,
)
return f"published to catalog + ARD indexed ({ard.status_code})"
def build_zip(ctx: RunContext, plugin_id: str) -> str:
"""Create a signed, portable .zip archive of the plugin directory."""
pkg = PLUGIN_ROOT / plugin_id
out = PLUGIN_ROOT / f"{plugin_id}-{pkg.joinpath('plugin.json').read_text() and json.loads((pkg / 'plugin.json').read_text())['version']}.zip"
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
for file in pkg.rglob("*"):
if file.is_file() and not file.name.endswith(".env"):
zf.write(file, file.relative_to(pkg))
return str(out)
plugin_tools = [
Tool(validate_manifest),
Tool(smoke_test),
Tool(publish_plugin),
Tool(build_zip),
]
Now the LangGraph state machine wires the stages together, with a retry node for failed manifests:
# graph.py
from __future__ import annotations
import os
from dataclasses import dataclass, field
from langgraph.graph import END, START, StateGraph
from langgraph.graph.state import CompiledStateGraph
from pydantic_ai import Agent
from tools import PLUGIN_ROOT, plugin_tools, load_manifest, publish_plugin, smoke_test
@dataclass
class PipelineState:
plugin_id: str
attempts: int = 0
smoke_results: list[str] = field(default_factory=list)
archive: str | None = None
status: str = "pending"
builder_agent = Agent(
"openai:gpt-5.6-luna",
system_prompt=(
"You are the Agent Plugins 1.0 build orchestrator. Run the validation,"
"smoke test, and publish stages in order and never skip validation."
),
tools=plugin_tools,
model_settings={"max_steps": 40},
)
def stage_scaffold(state: PipelineState) -> PipelineState:
return state
def stage_validate(state: PipelineState) -> PipelineState:
result = builder_agent.run_sync(f"validate plugin {state.plugin_id}")
state.status = result.data
state.attempts += 1
return state
def stage_smoke(state: PipelineState) -> PipelineState:
clients = os.environ["SMOKE_CLIENTS"].split(",")
state.smoke_results = [smoke_test.__wrapped__(builder_agent, state.plugin_id, c) for c in clients]
state.status = "passed" if all("PASS" in r for r in state.smoke_results) else "failed"
return state
def stage_publish(state: PipelineState) -> PipelineState:
archive = __import__("tools").build_zip.__wrapped__(builder_agent, state.plugin_id)
state.archive = archive
state.status = publish_plugin.__wrapped__(builder_agent, state.plugin_id, archive)
return state
def should_retry(state: PipelineState) -> str:
if state.status == "passed":
return "publish"
if state.attempts < int(os.environ["MAX_MANIFEST_ATTEMPTS"]):
return "validate"
return "end_failed"
def build_pipeline() -> CompiledStateGraph:
g = StateGraph(PipelineState)
g.add_node("scaffold", stage_scaffold)
g.add_node("validate", stage_validate)
g.add_node("smoke", stage_smoke)
g.add_node("publish", stage_publish)
g.add_edge(START, "scaffold")
g.add_edge("scaffold", "validate")
g.add_edge("validate", "smoke")
g.add_conditional_edges("smoke", should_retry, {"validate": "validate", "publish": "publish", "end_failed": END})
g.add_edge("publish", END)
return g.compile()
Entry point:
# main.py
from graph import build_pipeline
if __name__ == "__main__":
pipeline = build_pipeline()
final = pipeline.invoke({"plugin_id": "org.acme.support-agent"})
print(final.status)
for line in final.smoke_results:
print(line)
Semantic Validation Rules
Validation is where most hand-rolled pipelines die, so the checks are explicit and testable. The validator runs four layers, in order:
| Layer | Scope | Failure behavior |
|---|---|---|
| Syntactic | JSON parses, UTF-8 clean | Hard fail, no retry |
| Structural | Pydantic schema mirror of the 1.0.0 manifest | Retry up to 3 attempts |
| Semantic | entrypoint paths exist, transport matches server, no unresolved ${VAR} |
Retry, then fail the gate |
| Cross-client | version pins satisfy every declared host | Bump min_versions, re-run the matrix |
The semantic layer is the one that catches real-world breakage: a streamable-http server with no reachable URL, a stdio server whose command is missing from the runner, or a skill whose SKILL.md references a deleted file. Because each layer fails fast, a broken plugin never wastes a client harness cycle, and a manifest that reaches the smoke matrix has already passed a contract check rather than a best-effort parse.
Smoke Tests Across Real Clients
The value of the pipeline is the harness matrix. Each harness boots the host in headless mode, imports the plugin from the built directory, triggers one representative invocation per skill, and calls one MCP tool. A pass requires all of:
- the manifest parses and the host resolves entry points;
- every
SKILL.mdfront-matter block validates; - every declared MCP server connects with its declared transport;
- a seeded test task completes end to end.
The smoke tests deliberately pin client versions so a plugin can declare client.min_versions accurately. If Cursor pushes a breaking change, the matrix fails on Cursor only, and the manifest's min_versions.cursor is bumped rather than blocking the whole release. Treat the matrix as a contract, not a chore: store each harness result as a CI artifact keyed by client and version so regressions stay greppable, and keep one golden plugin that exercises every transport type. If a host refuses the golden plugin, the host, not the plugin, is usually at fault.
Publishing to the AI Catalog and ARD
The final stage publishes the signed archive to your catalog using the official MIME type application/agent-plugins+json and registers the resource with Google's Agentic Resource Discovery (ARD), which indexes Agent Plugins as a first-class resource type. Once registered, any ARD-aware client can discover the plugin without a hard-coded registry entry. Two 2026 ecosystem moves make this stage future-proof:
- Skills Over MCP bundles skills directly into an MCP server, so a plugin can expose skills via tools without the client understanding Agent Skills at all. The pipeline supports both layouts: a
skills/directory for native hosts and an MCPresources/tree for skill-over-MCP hosts. - Stateless MCP 2026-07-28 removed the handshake and session header, so the
mcp.jsonyou ship no longer needs session semantics. Each request self-describes via_meta, which is exactly why the plugin declares transport type explicitly:stdio,streamable-http, or the now-deprecatedhttp+sse.
Versioning follows semver on the manifest itself. A breaking change to skills or MCP servers bumps the major version, and the catalog retains every published version, so hosts can pin to a known-good release while the new one rolls through the smoke matrix. The catalog entry also stores checksums and the license identifier (CC-BY-4.0 for the spec itself; your plugin can choose its own), which keeps legal and supply-chain review simple.
Retry & Resilience Rules
Distributed validation and publishing fail constantly. The pipeline applies explicit rules:
- Backoff policy:
base 2s,factor 2.0,max 30s, full jitter between attempts. - Manifest validation: up to 3 attempts; a validation error increments the attempt counter but never enters the smoke matrix.
- Smoke tests: per-client timeout of 300s; a single client timeout retries that harness up to 2 times, then fails the gate.
- Publish idempotency: publishing uses
X-Plugin-Id+ version as the idempotency key; a retried publish with the same key is a no-op returning the existing catalog entry. - Secret safety: any
${VAR}left unresolved at publish time fails the build. Secrets are never written into the archive. - Circuit breaker: if the catalog API returns 5xx three times in five minutes, the pipeline pauses publishing and alerts on-call.
These rules make the difference between a demo script and a pipeline you can run at release time.
FAQ
Is Agent Plugins 1.0 only for OpenAI-compatible clients?
No. The spec is vendor-neutral and CC-BY-4.0 licensed, stewarded by the Agentic AI Foundation with Core Maintainers from Amazon, Cursor, Microsoft, OpenAI, Vercel, and Google. VS Code, Copilot, ChatGPT, Codex, Kiro, AWS Agent Toolkit, and Google Data Agent Kit already support it.
Do I need an MCP server to ship a plugin?
No. A plugin must expose at least one entry point, which can be a skill-only package with no mcp.json. Skills Over MCP is an optional bundle where skills ride inside the MCP server.
What does ARD indexing give me?
ARD makes the plugin discoverable as a first-class resource across ARD-aware clients, so users can find and install the plugin without a central marketplace, and updates propagate automatically.
Conclusion
Agent Plugins 1.0 turns agent distribution into a normal software engineering problem. With PydanticAI schemas mirroring the spec, a LangGraph pipeline that validates before it ships, a multi-client smoke matrix, and a MIME-typed catalog indexed by ARD, you can release portable agent capabilities the same way you release libraries. Keep pace with the ecosystem in the Daily AI World AI Workflows library and the latest AI news.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Inference Spending Surpasses Training for First Time
Next Story →GitHub Enterprise Rolls Out Strict MCP Allowlists
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...