diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 0000000..1ee09c1 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "paper", + "owner": { + "name": "Paper", + "email": "team@paper.design" + }, + "metadata": { + "description": "Paper connects your teams, agents, code, and data on a single design space built on web standards.", + "version": "0.1.0" + }, + "plugins": [ + { + "name": "paper-desktop", + "source": "./plugins/paper-desktop", + "description": "Connect to Paper Desktop using its MCP server.", + "version": "0.2.0" + } + ] +} diff --git a/README.md b/README.md index e2e0a9d..2eb6f05 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,51 @@ codex plugin install paper-desktop@paper You can also browse and install plugins interactively by running `/plugins` inside Codex CLI after adding the marketplace. - [Read more about installing Codex plugins](https://developers.openai.com/codex/plugins) + +## Copilot CLI + +**Add the custom marketplace** + +```sh +copilot plugin marketplace add paper-design/agent-plugins +``` + +**Install the plugin** + +```sh +copilot plugin install paper-desktop@paper +``` + +Confirm with `copilot mcp list`. Plugins installed this way also appear in VS Code under **Agent Plugins - Installed**. + +## VS Code + +1. Enable agent plugins: set `chat.plugins.enabled` to `true`. +2. Add the marketplace in settings: + +```json +"chat.plugins.marketplaces": [ + "paper-design/agent-plugins" +] +``` + +3. Install **paper-desktop** from the Agent Plugins view (or install via Copilot CLI as above). + +### Manual MCP (optional) + +If you prefer not to use plugins, add this to your Copilot / VS Code MCP config: + +```json +{ + "servers": { + "paper": { + "type": "stdio", + "command": "${userHome}/.paper/bin/paper", + "args": ["mcp"] + } + } +} +``` + +- [About Copilot plugins](https://docs.github.com/en/copilot/concepts/agents/about-plugins) +- [Agent plugins in VS Code](https://code.visualstudio.com/docs/agent-customization/agent-plugins) diff --git a/package.json b/package.json index 4448776..b0b1510 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "type": "module", "private": true, "scripts": { - "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-codex.mjs" + "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-codex.mjs && bun ./scripts/validate-copilot.mjs", + "test": "bun run build" }, "devDependencies": { "@types/bun": "latest", diff --git a/plugins/paper-desktop/.claude-plugin/plugin.json b/plugins/paper-desktop/.claude-plugin/plugin.json index bdb45a5..dd80a5d 100644 --- a/plugins/paper-desktop/.claude-plugin/plugin.json +++ b/plugins/paper-desktop/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "paper-desktop", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Claude can read and write to — built on web standards.", "author": { "name": "Paper", @@ -21,6 +21,5 @@ "mcp", "design-system" ], - "skills": "./skills", "mcpServers": "./mcp.json" } diff --git a/plugins/paper-desktop/.codex-plugin/plugin.json b/plugins/paper-desktop/.codex-plugin/plugin.json index 18d14da..9234f81 100644 --- a/plugins/paper-desktop/.codex-plugin/plugin.json +++ b/plugins/paper-desktop/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "paper-desktop", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Codex can read and write to — built on web standards.", "author": { "name": "Paper", @@ -23,7 +23,6 @@ "mcp", "design-system" ], - "skills": "./skills/", "mcpServers": "./mcp.json", "interface": { "displayName": "Paper Desktop", diff --git a/plugins/paper-desktop/.cursor-plugin/plugin.json b/plugins/paper-desktop/.cursor-plugin/plugin.json index f36e0ef..2833fb3 100644 --- a/plugins/paper-desktop/.cursor-plugin/plugin.json +++ b/plugins/paper-desktop/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "paper-desktop", "displayName": "Paper", - "version": "0.1.0", + "version": "0.2.0", "description": "Design on a canvas that Cursor can read and write to — built on web standards.", "author": { "name": "Paper", diff --git a/plugins/paper-desktop/README.md b/plugins/paper-desktop/README.md index c285d22..a718093 100644 --- a/plugins/paper-desktop/README.md +++ b/plugins/paper-desktop/README.md @@ -14,7 +14,7 @@ Paper connects your designs, agents, code, and data on a single canvas built on ## Prerequisites -Paper Desktop must be running with a file open for the MCP server to be available. Download it at [paper.design/downloads](https://paper.design/downloads). +Install [Paper Desktop](https://paper.design/downloads) and open it once. ## Examples diff --git a/plugins/paper-desktop/mcp.json b/plugins/paper-desktop/mcp.json index 034d089..3afdae1 100644 --- a/plugins/paper-desktop/mcp.json +++ b/plugins/paper-desktop/mcp.json @@ -1,8 +1,10 @@ { + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "mcpServers": { "paper": { - "type": "http", - "url": "http://127.0.0.1:29979/mcp" + "type": "stdio", + "command": "${userHome}/.paper/bin/paper", + "args": ["mcp"] } } } diff --git a/plugins/paper-desktop/plugin.json b/plugins/paper-desktop/plugin.json new file mode 100644 index 0000000..b56675f --- /dev/null +++ b/plugins/paper-desktop/plugin.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "paper-desktop", + "version": "0.2.0", + "description": "Design on a canvas that Copilot can read and write to — built on web standards.", + "author": { + "name": "Paper", + "email": "team@paper.design", + "url": "https://paper.design" + }, + "homepage": "https://paper.design", + "repository": "https://github.com/paper-design/agent-plugins", + "license": "MIT", + "keywords": [ + "paper", + "design", + "ui", + "canvas", + "html", + "css", + "design-to-code", + "code-to-design", + "mcp", + "design-system" + ], + "mcpServers": "./mcp.json" +} diff --git a/plugins/paper-desktop/rules/ensure-paper-started.md b/plugins/paper-desktop/rules/ensure-paper-started.md deleted file mode 100644 index 71eff56..0000000 --- a/plugins/paper-desktop/rules/ensure-paper-started.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -description: Guidelines for using the Paper MCP server ---- - -Before using any Paper MCP tools, ensure the Paper Desktop app is running on the user's machine. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. diff --git a/plugins/paper-desktop/skills/code-to-design/SKILL.md b/plugins/paper-desktop/skills/code-to-design/SKILL.md deleted file mode 100644 index 2ba0ace..0000000 --- a/plugins/paper-desktop/skills/code-to-design/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: code-to-design -description: Generate a Paper design from the project's codebase — using its tokens, styles, and components as context. ---- - -Before starting, ensure Paper Desktop is running with a file open. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. - -Read the project's stylesheets, design tokens, or theme files to understand the existing design language. Then create a new artboard in Paper and build the requested UI using the codebase's actual colors, typography, spacing, and component patterns — not generic defaults. diff --git a/plugins/paper-desktop/skills/design-to-code/SKILL.md b/plugins/paper-desktop/skills/design-to-code/SKILL.md deleted file mode 100644 index d3b68a3..0000000 --- a/plugins/paper-desktop/skills/design-to-code/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: design-to-code -description: Turn a Paper design into production code using the project's existing conventions. ---- - -Before starting, ensure Paper Desktop is running with a file open. If a connection to the Paper MCP server fails, remind the user to open Paper Desktop first. - -Read the selected frame or artboard from Paper — including structure, styles, text content, and computed layout. Then generate production-ready components in the project's framework and coding style. Match the design's layout, spacing, typography, and colors using the codebase's existing conventions (e.g. Tailwind classes, CSS modules, design tokens, styled-components). diff --git a/schemas/agent-plugins/mcp.schema.json b/schemas/agent-plugins/mcp.schema.json new file mode 100644 index 0000000..a9139a4 --- /dev/null +++ b/schemas/agent-plugins/mcp.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "title": "Agent Plugins MCP Configuration", + "description": "Machine-readable schema for mcp.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.", + "type": "object", + "properties": { + "$schema": { + "const": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "description": "Canonical identifier of the MCP configuration schema for the Agent Plugins version targeted by this document." + }, + "mcpServers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server" + } + } + }, + "required": ["$schema", "mcpServers"], + "additionalProperties": false, + "$defs": { + "server": { + "title": "MCP server", + "oneOf": [ + { + "$ref": "#/$defs/stdioServer" + }, + { + "$ref": "#/$defs/streamableHttpServer" + }, + { + "$ref": "#/$defs/sseServer" + } + ] + }, + "stdioServer": { + "title": "stdio MCP server", + "type": "object", + "properties": { + "type": { + "const": "stdio" + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable token. Resolution rules are defined by the Agent Plugins specification." + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "propertyNames": { + "not": { + "enum": ["PLUGIN_ROOT", "PLUGIN_DATA"] + } + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "type": "string", + "pattern": "^(?:\\./|\\$\\{PLUGIN_ROOT\\}(?:/|$)|\\$\\{PLUGIN_DATA\\}(?:/|$))", + "description": "Plugin-relative, PLUGIN_ROOT-rooted, or PLUGIN_DATA-rooted working directory. Filesystem containment is validated separately." + } + }, + "required": ["type", "command"], + "additionalProperties": false + }, + "streamableHttpServer": { + "title": "Streamable HTTP MCP server", + "type": "object", + "properties": { + "type": { + "const": "streamable-http" + }, + "url": { + "type": "string", + "minLength": 1, + "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification." + }, + "headers": { + "$ref": "#/$defs/headers" + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "sseServer": { + "title": "Legacy HTTP+SSE MCP server", + "type": "object", + "properties": { + "type": { + "const": "sse" + }, + "url": { + "type": "string", + "minLength": 1, + "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification." + }, + "headers": { + "$ref": "#/$defs/headers" + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "headers": { + "title": "HTTP headers", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/schemas/agent-plugins/plugin.schema.json b/schemas/agent-plugins/plugin.schema.json new file mode 100644 index 0000000..8fed0e1 --- /dev/null +++ b/schemas/agent-plugins/plugin.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "title": "Agent Plugins Manifest", + "description": "Machine-readable schema for plugin.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.", + "type": "object", + "properties": { + "$schema": { + "const": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "description": "Canonical identifier of the plugin manifest schema for the Agent Plugins version targeted by this document." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", + "description": "Human-readable plugin name." + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "homepage": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "license": { + "type": "string" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "type": "object", + "description": "Client-specific manifest data keyed by reverse-domain extension namespace. Agent Plugins assigns no semantics to namespace object contents.", + "additionalProperties": { + "type": "object" + } + } + }, + "required": ["$schema", "name"], + "additionalProperties": false +} diff --git a/scripts/validate-copilot.mjs b/scripts/validate-copilot.mjs new file mode 100644 index 0000000..c96b597 --- /dev/null +++ b/scripts/validate-copilot.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +/** + * Copilot plugin validator. + * - marketplace.json: GitHub Copilot marketplace layout + * - plugin.json: Open Plugin Spec fields (+ Copilot `mcpServers` path) + * - mcp.json: Open Plugin Spec MCP schema + */ + +import { readFileSync, existsSync, statSync } from "fs"; +import { resolve, dirname, relative } from "path"; +import { fileURLToPath } from "url"; +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); + +function loadJSON(path) { + return JSON.parse(readFileSync(path, "utf-8")); +} + +function loadJSONSafe(path, label, fail) { + if (!existsSync(path)) { + fail(`${label}: file not found at ${relative(root, path)}`); + return null; + } + try { + return loadJSON(path); + } catch (err) { + fail(`${label}: invalid JSON — ${err.message}`); + return null; + } +} + +const pluginSchema = loadJSON( + resolve(root, "schemas/agent-plugins/plugin.schema.json") +); +const mcpSchema = loadJSON(resolve(root, "schemas/agent-plugins/mcp.schema.json")); + +const ajv = new Ajv2020({ allErrors: true, strict: false }); +addFormats(ajv); + +const validatePlugin = ajv.compile(pluginSchema); +const validateMcp = ajv.compile(mcpSchema); + +let errors = 0; + +function fail(message) { + console.error(`ERROR: ${message}`); + errors++; +} + +function reportSchemaErrors(label, validate) { + fail(`${label}: schema validation failed:`); + for (const err of validate.errors ?? []) { + const detail = + err.keyword === "additionalProperties" + ? `${err.message}: "${err.params.additionalProperty}"` + : err.message; + console.error(` ${err.instancePath || "/"}: ${detail}`); + } +} + +function isValidRelativePath(path) { + if (typeof path !== "string" || path.length === 0) return false; + const stripped = path.startsWith("./") ? path.slice(2) : null; + if (stripped === null || stripped.length === 0) return false; + const segments = stripped.replace(/\/+$/, "").split("/"); + return segments.every((s) => s.length > 0 && s !== ".." && s !== "."); +} + +// 1. Marketplace (GitHub Copilot layout; structural checks) +const marketplacePath = resolve(root, ".github/plugin/marketplace.json"); +const marketplace = loadJSONSafe(marketplacePath, "Marketplace", fail); + +if (!marketplace) { + process.exit(1); +} + +if (typeof marketplace.name !== "string" || marketplace.name.length === 0) { + fail("Marketplace: missing or empty `name`"); +} + +if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length === 0) { + fail("Marketplace: `plugins` must be a non-empty array"); +} + +for (const [index, entry] of (marketplace.plugins ?? []).entries()) { + const label = `Marketplace plugins[${index}]`; + + if (typeof entry?.name !== "string" || entry.name.length === 0) { + fail(`${label}: missing or empty \`name\``); + continue; + } + + const pluginLabel = `Marketplace plugin "${entry.name}"`; + + if (!isValidRelativePath(entry.source)) { + fail( + `${pluginLabel}: \`source\` must be a relative path starting with "./" — got "${entry.source}"` + ); + continue; + } + + const pluginDir = resolve(root, entry.source.slice(2)); + if (!existsSync(pluginDir) || !statSync(pluginDir).isDirectory()) { + fail(`${pluginLabel}: source directory does not exist — ${entry.source}`); + continue; + } + + // 2. plugin.json — OPS core fields; Copilot allows mcpServers on top + const pluginJsonPath = resolve(pluginDir, "plugin.json"); + const pluginJson = loadJSONSafe( + pluginJsonPath, + `${pluginLabel} plugin.json`, + fail + ); + if (!pluginJson) continue; + + const { mcpServers, ...opsPluginJson } = pluginJson; + if (!validatePlugin(opsPluginJson)) { + reportSchemaErrors( + `${pluginLabel} (${relative(root, pluginJsonPath)})`, + validatePlugin + ); + } + + if (pluginJson.name && pluginJson.name !== entry.name) { + fail( + `${pluginLabel}: marketplace name does not match plugin.json name "${pluginJson.name}"` + ); + } + + // 3. MCP via Copilot mcpServers path (or inline object) + if (mcpServers === undefined) { + fail( + `${pluginLabel}: missing \`mcpServers\` (path to mcp.json or inline servers)` + ); + continue; + } + + let mcpJson = null; + let mcpLabel = `${pluginLabel} mcpServers`; + + if (typeof mcpServers === "string") { + if (!isValidRelativePath(mcpServers)) { + fail( + `${pluginLabel}: \`mcpServers\` path must start with "./" — got "${mcpServers}"` + ); + continue; + } + const mcpPath = resolve(pluginDir, mcpServers.slice(2)); + mcpLabel = `${pluginLabel} (${relative(root, mcpPath)})`; + mcpJson = loadJSONSafe(mcpPath, mcpLabel, fail); + } else if (mcpServers && typeof mcpServers === "object") { + mcpJson = { mcpServers }; + // Inline Copilot shape lacks OPS $schema; wrap only the servers object + // Validate by synthesizing a minimal OPS document when possible. + if (!("$schema" in mcpServers) && !("mcpServers" in mcpServers)) { + mcpJson = { + $schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + mcpServers, + }; + } else { + mcpJson = mcpServers; + } + } else { + fail(`${pluginLabel}: \`mcpServers\` must be a path string or object`); + continue; + } + + if (!mcpJson) continue; + + if (!validateMcp(mcpJson)) { + reportSchemaErrors(mcpLabel, validateMcp); + } +} + +if (errors > 0) { + console.error(`\nCopilot validation failed with ${errors} error(s).`); + process.exit(1); +} + +console.log("Copilot validation passed."); +process.exit(0);