From 73943a0c9dc77ce9641be0dd74e94141872d867b Mon Sep 17 00:00:00 2001 From: joseph Date: Mon, 10 Aug 2026 16:57:59 -0700 Subject: [PATCH] Add Agent Plugins starter template Co-authored-by: Cursor --- .cursor-plugin/marketplace.json | 5 + README.md | 17 +- docs/add-a-plugin.md | 49 ++++- plugins/starter-agent-plugins/README.md | 24 +++ plugins/starter-agent-plugins/mcp.json | 9 + plugins/starter-agent-plugins/plugin.json | 13 ++ .../skills/code-reviewer/SKILL.md | 20 ++ scripts/validate-template.mjs | 194 ++++++++++++++++-- 8 files changed, 304 insertions(+), 27 deletions(-) create mode 100644 plugins/starter-agent-plugins/README.md create mode 100644 plugins/starter-agent-plugins/mcp.json create mode 100644 plugins/starter-agent-plugins/plugin.json create mode 100644 plugins/starter-agent-plugins/skills/code-reviewer/SKILL.md diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 5943c88..2918646 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -18,6 +18,11 @@ "name": "starter-advanced", "source": "./plugins/starter-advanced", "description": "Full-featured plugin with rules, skills, agents, commands, hooks, and MCP" + }, + { + "name": "starter-agent-plugins", + "source": "./plugins/starter-agent-plugins", + "description": "Minimal Agent Plugin with skills and MCP" } ] } diff --git a/README.md b/README.md index a184d56..a6c9df7 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,22 @@ Build and publish Cursor Marketplace plugins from a single repo. -Two starter plugins are included: +This template includes both [Cursor Plugins](https://cursor.com/docs/plugins) and an [Agent Plugins](https://agent-plugins.org) starter: -- **starter-simple**: rules and skills only -- **starter-advanced**: rules, skills, agents, commands, hooks, MCP, and scripts +- **starter-simple**: Cursor Plugin with rules and skills only +- **starter-advanced**: Cursor Plugin with rules, skills, agents, commands, hooks, MCP, and scripts +- **starter-agent-plugins**: Agent Plugins format with a root `plugin.json`, skills, and MCP servers + +Cursor Plugins use `.cursor-plugin/plugin.json` and can bundle Cursor-specific components. Agent Plugins use a root `plugin.json` with the standard `$schema` and package portable skills and MCP servers. ## Getting started [Use this template](https://github.com/cursor/plugin-template/generate) to create a new repository, then customize: 1. `.cursor-plugin/marketplace.json`: set marketplace `name`, `owner`, and `metadata`. -2. `plugins/*/.cursor-plugin/plugin.json`: set `name` (lowercase kebab-case), `displayName`, `author`, `description`, `keywords`, `license`, and `version`. -3. Replace placeholder rules, skills, agents, commands, hooks, scripts, and logos. +2. For a Cursor Plugin, edit `plugins/*/.cursor-plugin/plugin.json`: set `name` (lowercase kebab-case), `displayName`, `author`, `description`, `keywords`, `license`, and `version`. +3. For an Agent Plugin, edit `plugins/*/plugin.json`: keep `$schema` and set `name`, `author`, `description`, `keywords`, `license`, and `version`. +4. Replace placeholder rules, skills, agents, commands, hooks, scripts, MCP servers, and logos. To add more plugins, see `docs/add-a-plugin.md`. @@ -25,7 +29,8 @@ For a **single plugin**, move your plugin folder contents to the repository root ## Submission checklist -- Each plugin has a valid `.cursor-plugin/plugin.json`. +- Each Cursor Plugin has a valid `.cursor-plugin/plugin.json`. +- Each Agent Plugin has a valid root `plugin.json` with `$schema` set to `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`. - Plugin names are unique, lowercase, and kebab-case. - `.cursor-plugin/marketplace.json` entries map to real plugin folders. - All frontmatter metadata is present in rule, skill, agent, and command files. diff --git a/docs/add-a-plugin.md b/docs/add-a-plugin.md index 822bd30..7dd7fda 100644 --- a/docs/add-a-plugin.md +++ b/docs/add-a-plugin.md @@ -2,6 +2,11 @@ Add a new plugin under `plugins/` and register it in `.cursor-plugin/marketplace.json`. +Choose a format first: + +- **Cursor Plugin**: `.cursor-plugin/plugin.json`, plus any of rules, skills, agents, commands, hooks, and MCP +- **Agent Plugin**: root `plugin.json` with the Agent Plugins `$schema`, plus optional `skills/` and `mcp.json` + ## 1. Create plugin directory Create a new folder: @@ -10,7 +15,7 @@ Create a new folder: plugins/my-new-plugin/ ``` -Add the required manifest: +### Cursor Plugin manifest ```text plugins/my-new-plugin/.cursor-plugin/plugin.json @@ -31,9 +36,33 @@ Example manifest: } ``` +### Agent Plugin manifest + +```text +plugins/my-new-plugin/plugin.json +``` + +Example manifest: + +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "my-new-plugin", + "version": "1.0.0", + "description": "Describe what this plugin does", + "author": { + "name": "Your Org" + } +} +``` + +The Agent Plugins schema is closed. Do not add `displayName`, `logo`, `hooks`, `rules`, or other Cursor-only fields at the top level. + ## 2. Add plugin components -Add only the components you need: +Add only the components you need. + +### Cursor Plugin components - `rules/` with `.mdc` files (YAML frontmatter required) - `skills//SKILL.md` (YAML frontmatter required) @@ -43,6 +72,13 @@ Add only the components you need: - `mcp.json` for MCP server definitions - `assets/logo.svg` for marketplace display +### Agent Plugin components + +- `skills//SKILL.md` (YAML frontmatter required) +- `mcp.json` for MCP server definitions + +`mcp.json` must include `$schema`, and each server must declare `type` (`stdio`, `streamable-http`, or `sse`). Agent Plugins do not include rules, agents, commands, hooks, or logos. + ## 3. Register in marketplace manifest Edit `.cursor-plugin/marketplace.json` and append a new entry: @@ -69,8 +105,15 @@ Fix all reported errors before committing. - Plugin `name` not kebab-case. - `source` path in marketplace manifest does not match folder name. -- Missing `.cursor-plugin/plugin.json` in plugin folder. +- Missing `.cursor-plugin/plugin.json` for a Cursor Plugin, or missing root `plugin.json` `$schema` for an Agent Plugin. +- Adding Cursor-only fields (`displayName`, `logo`, `rules`) to an Agent Plugin `plugin.json`. +- Agent Plugin `mcp.json` missing `$schema` or server `type`. - Missing frontmatter keys (`name`, `description`) in skills, agents, or commands. - Rule files missing frontmatter `description`. - Using a filename other than `mcp.json` for MCP server definitions. - Broken relative paths for `logo`, `hooks`, or `mcpServers` in manifest files. + +## 6. More resources + +- [Cursor Plugins documentation](https://cursor.com/docs/plugins) +- [Agent Plugins](https://agent-plugins.org/) diff --git a/plugins/starter-agent-plugins/README.md b/plugins/starter-agent-plugins/README.md new file mode 100644 index 0000000..ed5f5b6 --- /dev/null +++ b/plugins/starter-agent-plugins/README.md @@ -0,0 +1,24 @@ +# starter-agent-plugins + +Simple [Agent Plugins](https://agent-plugins.org) starter. This format uses a root `plugin.json` and can package skills and MCP servers that load in any spec-compatible client, including Cursor. + +```text +starter-agent-plugins/ +├── plugin.json +├── skills/ +│ └── code-reviewer/ +│ └── SKILL.md +└── mcp.json +``` + +## Included + +- `plugin.json`: Agent Plugins 1.0.0 manifest (`$schema` is required) +- `skills/code-reviewer/`: one example [Agent Skill](https://agentskills.io/specification) +- `mcp.json`: one example remote MCP server (`type` is required) + +## Not included + +Agent Plugins 1.0.0 does not define rules, agents, commands, hooks, logos, or marketplace fields. For those Cursor-specific components, use `starter-simple` or `starter-advanced`. + +Client-owned extras belong under `extensions` or a reverse-domain directory. See the [client extensions guide](https://agent-plugins.org/plugin-authors/client-extensions). diff --git a/plugins/starter-agent-plugins/mcp.json b/plugins/starter-agent-plugins/mcp.json new file mode 100644 index 0000000..bc4591a --- /dev/null +++ b/plugins/starter-agent-plugins/mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "remote-tools": { + "type": "streamable-http", + "url": "https://tools.example.com/mcp" + } + } +} diff --git a/plugins/starter-agent-plugins/plugin.json b/plugins/starter-agent-plugins/plugin.json new file mode 100644 index 0000000..f7ffb63 --- /dev/null +++ b/plugins/starter-agent-plugins/plugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "starter-agent-plugins", + "version": "1.0.0", + "description": "Minimal Agent Plugin starter with skills and MCP servers.", + "author": { + "name": "Your Org", + "email": "plugins@example.com" + }, + "license": "MIT", + "homepage": "https://agent-plugins.org/", + "keywords": ["agent-plugins", "skills", "mcp"] +} diff --git a/plugins/starter-agent-plugins/skills/code-reviewer/SKILL.md b/plugins/starter-agent-plugins/skills/code-reviewer/SKILL.md new file mode 100644 index 0000000..fbcc365 --- /dev/null +++ b/plugins/starter-agent-plugins/skills/code-reviewer/SKILL.md @@ -0,0 +1,20 @@ +--- +name: code-reviewer +description: Review code for correctness, maintainability, and security risks. Use when preparing a PR or auditing a change set. +--- + +# Code reviewer + +## When to use + +- Before opening a pull request +- After large refactors +- When validating risky changes + +## Instructions + +1. Identify potential behavioral regressions first. +2. Flag security concerns (XSS, SQL injection, command injection, secrets). +3. Evaluate readability, structure, and naming consistency. +4. Recommend concrete fixes with minimal churn. +5. Call out missing tests or validation steps. diff --git a/scripts/validate-template.mjs b/scripts/validate-template.mjs index 5310b9e..f4710c8 100644 --- a/scripts/validate-template.mjs +++ b/scripts/validate-template.mjs @@ -9,7 +9,24 @@ const errors = []; const warnings = []; const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/; +const agentPluginNamePattern = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/; const marketplaceNamePattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; +const agentPluginSchema = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"; +const agentMcpSchema = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"; +const agentPluginFields = new Set([ + "$schema", + "name", + "version", + "description", + "author", + "homepage", + "repository", + "license", + "keywords", + "extensions", +]); +const agentAuthorFields = new Set(["name", "email", "url"]); +const agentMcpTypes = new Set(["stdio", "streamable-http", "sse"]); function addError(message) { errors.push(message); @@ -232,6 +249,135 @@ async function validateComponentFrontmatter(pluginDir, pluginName) { } } +async function detectPluginFormat(pluginDir) { + const cursorManifestPath = path.join(pluginDir, ".cursor-plugin", "plugin.json"); + const agentManifestPath = path.join(pluginDir, "plugin.json"); + const hasCursorManifest = await pathExists(cursorManifestPath); + const hasAgentManifest = await pathExists(agentManifestPath); + + if (hasAgentManifest) { + const agentManifest = await readJsonFile(agentManifestPath, "plugin.json"); + if (!agentManifest) { + return { format: null, manifestPath: agentManifestPath, manifest: null, hasCursorManifest }; + } + if (agentManifest.$schema === agentPluginSchema) { + return { + format: "agent", + manifestPath: agentManifestPath, + manifest: agentManifest, + hasCursorManifest, + }; + } + if (!hasCursorManifest) { + addError( + `plugin.json is missing the Agent Plugins $schema (${agentPluginSchema}): ${agentManifestPath}` + ); + return { format: null, manifestPath: agentManifestPath, manifest: agentManifest, hasCursorManifest }; + } + } + + if (hasCursorManifest) { + const cursorManifest = await readJsonFile( + cursorManifestPath, + path.relative(repoRoot, cursorManifestPath) + ); + return { + format: "cursor", + manifestPath: cursorManifestPath, + manifest: cursorManifest, + hasCursorManifest, + }; + } + + addError( + `Missing plugin manifest. Add .cursor-plugin/plugin.json for a Cursor Plugin or plugin.json with $schema for an Agent Plugin: ${pluginDir}` + ); + return { format: null, manifestPath: null, manifest: null, hasCursorManifest }; +} + +function validateAgentPluginManifest(pluginManifest, pluginName) { + if (pluginManifest.$schema !== agentPluginSchema) { + addError(`${pluginName}: plugin.json "$schema" must be "${agentPluginSchema}".`); + } + + if (typeof pluginManifest.name !== "string" || !agentPluginNamePattern.test(pluginManifest.name)) { + addError( + `${pluginName}: "name" in plugin.json must be 1-64 chars, lowercase, and use only alphanumerics, hyphens, and periods without "--" or "..".` + ); + } + + for (const key of Object.keys(pluginManifest)) { + if (!agentPluginFields.has(key)) { + addWarning( + `${pluginName}: unknown plugin.json field "${key}" is not part of the Agent Plugins schema and will be ignored.` + ); + } + } + + if (pluginManifest.author !== undefined) { + if (!pluginManifest.author || typeof pluginManifest.author !== "object" || Array.isArray(pluginManifest.author)) { + addError(`${pluginName}: "author" must be an object with optional name, email, and url.`); + } else { + for (const key of Object.keys(pluginManifest.author)) { + if (!agentAuthorFields.has(key)) { + addError(`${pluginName}: "author.${key}" is not allowed. Use name, email, or url.`); + } else if (typeof pluginManifest.author[key] !== "string") { + addError(`${pluginName}: "author.${key}" must be a string.`); + } + } + } + } + + if (pluginManifest.keywords !== undefined && !Array.isArray(pluginManifest.keywords)) { + addError(`${pluginName}: "keywords" must be an array of strings.`); + } +} + +async function validateAgentMcpConfig(pluginDir, pluginName) { + const mcpPath = path.join(pluginDir, "mcp.json"); + if (!(await pathExists(mcpPath))) { + return; + } + + const mcpConfig = await readJsonFile(mcpPath, `${pluginName} mcp.json`); + if (!mcpConfig) { + return; + } + + if (mcpConfig.$schema !== agentMcpSchema) { + addError(`${pluginName}: mcp.json "$schema" must be "${agentMcpSchema}".`); + } + + if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object" || Array.isArray(mcpConfig.mcpServers)) { + addError(`${pluginName}: mcp.json must contain an "mcpServers" object.`); + return; + } + + for (const [serverName, server] of Object.entries(mcpConfig.mcpServers)) { + if (!server || typeof server !== "object") { + addError(`${pluginName}: mcpServers.${serverName} must be an object.`); + continue; + } + + if (!agentMcpTypes.has(server.type)) { + addError( + `${pluginName}: mcpServers.${serverName}.type must be "stdio", "streamable-http", or "sse".` + ); + continue; + } + + if (server.type === "stdio") { + if (typeof server.command !== "string" || server.command.length === 0) { + addError(`${pluginName}: mcpServers.${serverName}.command is required for stdio servers.`); + } else if (server.command.startsWith("./")) { + await validateReferencedPath(pluginDir, `mcpServers.${serverName}.command`, server.command, pluginName); + } + } else if (typeof server.url !== "string" || server.url.length === 0) { + addError(`${pluginName}: mcpServers.${serverName}.url is required for ${server.type} servers.`); + } + } +} + function resolveMarketplaceSource(source, pluginRoot) { if (typeof source !== "string" || source.length === 0) { return null; @@ -316,13 +462,21 @@ async function main() { continue; } - const manifestPath = path.join(pluginDir, ".cursor-plugin", "plugin.json"); - const pluginManifest = await readJsonFile(manifestPath, `${entry.name} plugin manifest`); - if (!pluginManifest) { + const detected = await detectPluginFormat(pluginDir); + const pluginManifest = detected.manifest; + if (!detected.format || !pluginManifest) { continue; } - if (typeof pluginManifest.name !== "string" || !pluginNamePattern.test(pluginManifest.name)) { + if (detected.format === "agent" && detected.hasCursorManifest) { + addWarning( + `${entry.name}: found both plugin.json and .cursor-plugin/plugin.json. Validating as an Agent Plugin.` + ); + } + + if (detected.format === "agent") { + validateAgentPluginManifest(pluginManifest, entry.name); + } else if (typeof pluginManifest.name !== "string" || !pluginNamePattern.test(pluginManifest.name)) { addError( `${entry.name}: "name" in plugin.json must be lowercase and use only alphanumerics, hyphens, and periods.` ); @@ -334,25 +488,29 @@ async function main() { ); } - const manifestFields = ["logo", "rules", "skills", "agents", "commands", "hooks", "mcpServers"]; - for (const field of manifestFields) { - const values = extractPathValues(pluginManifest[field]); - for (const value of values) { - await validateReferencedPath(pluginDir, field, value, entry.name); + if (detected.format === "cursor") { + const manifestFields = ["logo", "rules", "skills", "agents", "commands", "hooks", "mcpServers"]; + for (const field of manifestFields) { + const values = extractPathValues(pluginManifest[field]); + for (const value of values) { + await validateReferencedPath(pluginDir, field, value, entry.name); + } } - } - await validateComponentFrontmatter(pluginDir, entry.name); + const hooksPath = path.join(pluginDir, "hooks", "hooks.json"); + if (!(await pathExists(hooksPath))) { + addWarning(`${entry.name}: no hooks/hooks.json file found (only needed when using hooks).`); + } - const hooksPath = path.join(pluginDir, "hooks", "hooks.json"); - if (!(await pathExists(hooksPath))) { - addWarning(`${entry.name}: no hooks/hooks.json file found (only needed when using hooks).`); + const mcpPath = path.join(pluginDir, "mcp.json"); + if (!(await pathExists(mcpPath))) { + addWarning(`${entry.name}: no mcp.json file found (only needed when using MCP servers).`); + } + } else { + await validateAgentMcpConfig(pluginDir, entry.name); } - const mcpPath = path.join(pluginDir, "mcp.json"); - if (!(await pathExists(mcpPath))) { - addWarning(`${entry.name}: no mcp.json file found (only needed when using MCP servers).`); - } + await validateComponentFrontmatter(pluginDir, entry.name); } summarizeAndExit();