diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ffea8f0..381c2e7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,8 +5,7 @@ "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" + "description": "Paper connects your teams, agents, code, and data on a single design space built on web standards." }, "plugins": [ { diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index ffea8f0..381c2e7 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,8 +5,7 @@ "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" + "description": "Paper connects your teams, agents, code, and data on a single design space built on web standards." }, "plugins": [ { diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 0000000..381c2e7 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,17 @@ +{ + "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." + }, + "plugins": [ + { + "name": "paper-desktop", + "source": "./plugins/paper-desktop", + "description": "Connect to Paper Desktop using its MCP server." + } + ] +} diff --git a/README.md b/README.md index 52adc98..46bdf39 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,51 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o ```sh /plugin install paper-desktop@paper ``` + +## 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 e30a069..22756a7 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" + "build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-copilot.mjs", + "test": "bun run build" }, "devDependencies": { "@types/bun": "latest", diff --git a/plugins/paper-desktop/mcp.json b/plugins/paper-desktop/mcp.json index 794d7b0..839bda0 100644 --- a/plugins/paper-desktop/mcp.json +++ b/plugins/paper-desktop/mcp.json @@ -1,6 +1,8 @@ { + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "mcpServers": { "paper": { + "type": "stdio", "command": "${HOME}/.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/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);