diff --git a/README.md b/README.md index cb11be9..8372dd3 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,16 @@ claude mcp add vapi-docs -- npx -y mcp-remote https://docs.vapi.ai/_mcp/server | [setup-webhook](./setup-webhook) | Configure server URLs to receive real-time call events | | [create-workflow](./create-workflow) | Build visual conversation workflows with branching logic | +## Machine-Readable Manifest + +Agents and MCP servers can discover the current skill catalog from [`skills.manifest.json`](./skills.manifest.json). The manifest lists every skill, its primary `SKILL.md`, reference files, and raw GitHub URLs for runtime retrieval. + +Validate manifest changes with: + +```bash +node scripts/validate-skills-manifest.mjs +``` + ## Configuration All skills require a Vapi API key. Set it as an environment variable: diff --git a/schemas/skills.manifest.schema.json b/schemas/skills.manifest.schema.json new file mode 100644 index 0000000..b30fbed --- /dev/null +++ b/schemas/skills.manifest.schema.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/VapiAI/skills/main/schemas/skills.manifest.schema.json", + "title": "Vapi Skills Manifest", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "name", "description", "repository", "skills"], + "properties": { + "$schema": { + "type": "string" + }, + "schemaVersion": { + "type": "string" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "repository": { + "type": "object", + "additionalProperties": false, + "required": ["owner", "name", "defaultBranch", "htmlUrl", "rawBaseUrl"], + "properties": { + "owner": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "defaultBranch": { + "type": "string", + "minLength": 1 + }, + "htmlUrl": { + "type": "string", + "format": "uri" + }, + "rawBaseUrl": { + "type": "string", + "format": "uri" + } + } + }, + "skills": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/skill" + } + } + }, + "$defs": { + "path": { + "type": "string", + "pattern": "^[A-Za-z0-9._/-]+$", + "not": { + "anyOf": [ + { "pattern": "^/" }, + { "pattern": "(^|/)\\.\\.(/|$)" } + ] + } + }, + "reference": { + "type": "object", + "additionalProperties": false, + "required": ["name", "path", "rawUrl"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "path": { + "$ref": "#/$defs/path" + }, + "rawUrl": { + "type": "string", + "format": "uri" + } + } + }, + "skill": { + "type": "object", + "additionalProperties": false, + "required": ["name", "title", "description", "path", "rawUrl", "tags", "references"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "path": { + "$ref": "#/$defs/path" + }, + "rawUrl": { + "type": "string", + "format": "uri" + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$" + } + }, + "references": { + "type": "array", + "items": { + "$ref": "#/$defs/reference" + } + } + } + } + } +} diff --git a/scripts/validate-skills-manifest.mjs b/scripts/validate-skills-manifest.mjs new file mode 100644 index 0000000..f80670b --- /dev/null +++ b/scripts/validate-skills-manifest.mjs @@ -0,0 +1,106 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const repoRoot = path.resolve(new URL('..', import.meta.url).pathname); +const manifestPath = path.join(repoRoot, 'skills.manifest.json'); +const marketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json'); + +const fail = (message) => { + console.error(message); + process.exitCode = 1; +}; + +const readJson = (filePath) => JSON.parse(readFileSync(filePath, 'utf8')); + +const isSafePath = (filePath) => + typeof filePath === 'string' && + filePath.length > 0 && + !path.isAbsolute(filePath) && + !filePath.split('/').includes('..'); + +const skillFrontmatterGet = (filePath) => { + const content = readFileSync(filePath, 'utf8'); + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) { + return {}; + } + + return Object.fromEntries( + match[1] + .split('\n') + .map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)) + .filter(Boolean) + .map(([, key, value]) => [key, value.replace(/^"|"$/g, '')]), + ); +}; + +const manifest = readJson(manifestPath); +const marketplace = readJson(marketplacePath); +const marketplaceSkills = new Set( + marketplace.plugins.flatMap((plugin) => + plugin.skills.map((skillPath) => skillPath.replace(/^\.\//, '')), + ), +); +const manifestNames = new Set(); + +if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) { + fail('skills.manifest.json must include at least one skill'); +} + +for (const skill of manifest.skills ?? []) { + if (!skill.name || manifestNames.has(skill.name)) { + fail(`duplicate or missing skill name: ${skill.name ?? ''}`); + continue; + } + manifestNames.add(skill.name); + + if (!marketplaceSkills.has(skill.name)) { + fail(`${skill.name} is missing from .claude-plugin/marketplace.json`); + } + + if (!isSafePath(skill.path)) { + fail(`${skill.name} has an unsafe path: ${skill.path}`); + continue; + } + + const localPath = path.join(repoRoot, skill.path); + if (!existsSync(localPath)) { + fail(`${skill.name} path does not exist: ${skill.path}`); + continue; + } + + const frontmatter = skillFrontmatterGet(localPath); + if (frontmatter.name !== skill.name) { + fail(`${skill.name} frontmatter name mismatch: ${frontmatter.name}`); + } + + if (!skill.rawUrl?.endsWith(skill.path)) { + fail(`${skill.name} rawUrl must end with ${skill.path}`); + } + + for (const reference of skill.references ?? []) { + if (!isSafePath(reference.path)) { + fail(`${skill.name} has an unsafe reference path: ${reference.path}`); + continue; + } + + if (!existsSync(path.join(repoRoot, reference.path))) { + fail(`${skill.name} reference does not exist: ${reference.path}`); + } + + if (!reference.rawUrl?.endsWith(reference.path)) { + fail(`${skill.name} reference rawUrl must end with ${reference.path}`); + } + } +} + +for (const marketplaceSkill of marketplaceSkills) { + if (!manifestNames.has(marketplaceSkill)) { + fail(`${marketplaceSkill} is missing from skills.manifest.json`); + } +} + +if (!process.exitCode) { + console.log(`Validated ${manifest.skills.length} Vapi skills`); +} diff --git a/skills.manifest.json b/skills.manifest.json new file mode 100644 index 0000000..bea7dc9 --- /dev/null +++ b/skills.manifest.json @@ -0,0 +1,110 @@ +{ + "$schema": "./schemas/skills.manifest.schema.json", + "schemaVersion": "2026-05-04", + "name": "vapi-skills", + "description": "Agent skills for building voice AI agents with Vapi.", + "repository": { + "owner": "VapiAI", + "name": "skills", + "defaultBranch": "main", + "htmlUrl": "https://github.com/VapiAI/skills", + "rawBaseUrl": "https://raw.githubusercontent.com/VapiAI/skills/main" + }, + "skills": [ + { + "name": "setup-api-key", + "title": "Vapi API Key Setup", + "description": "Guide users through obtaining and configuring a Vapi API key.", + "path": "setup-api-key/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-api-key/SKILL.md", + "tags": ["setup", "auth", "api-key"], + "references": [] + }, + { + "name": "create-assistant", + "title": "Vapi Assistant Creation", + "description": "Create and configure Vapi voice AI assistants with models, voices, transcribers, tools, hooks, and advanced settings.", + "path": "create-assistant/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/SKILL.md", + "tags": ["assistants", "voice-agent", "configuration"], + "references": [ + { + "name": "hooks", + "path": "create-assistant/references/hooks.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/references/hooks.md" + }, + { + "name": "providers", + "path": "create-assistant/references/providers.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/references/providers.md" + } + ] + }, + { + "name": "create-tool", + "title": "Vapi Tool Creation", + "description": "Create custom tools for Vapi voice assistants including function tools, API request tools, transfer call tools, end call tools, and integrations.", + "path": "create-tool/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-tool/SKILL.md", + "tags": ["tools", "integrations", "function-calling"], + "references": [ + { + "name": "tool-server", + "path": "create-tool/references/tool-server.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-tool/references/tool-server.md" + } + ] + }, + { + "name": "create-call", + "title": "Vapi Call Creation", + "description": "Create outbound phone calls, web calls, and batch calls using the Vapi API.", + "path": "create-call/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-call/SKILL.md", + "tags": ["calls", "outbound", "testing"], + "references": [] + }, + { + "name": "create-squad", + "title": "Vapi Squad Creation", + "description": "Create multi-assistant squads in Vapi with handoffs between specialized voice agents.", + "path": "create-squad/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-squad/SKILL.md", + "tags": ["squads", "handoffs", "multi-agent"], + "references": [] + }, + { + "name": "create-phone-number", + "title": "Vapi Phone Number Setup", + "description": "Set up and manage phone numbers in Vapi for inbound and outbound voice AI calls.", + "path": "create-phone-number/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-phone-number/SKILL.md", + "tags": ["phone-numbers", "telephony", "providers"], + "references": [] + }, + { + "name": "setup-webhook", + "title": "Vapi Webhook / Server URL Setup", + "description": "Configure Vapi server URLs and webhooks to receive real-time call events, transcripts, tool calls, and end-of-call reports.", + "path": "setup-webhook/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-webhook/SKILL.md", + "tags": ["webhooks", "server-url", "events"], + "references": [ + { + "name": "webhook-events", + "path": "setup-webhook/references/webhook-events.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-webhook/references/webhook-events.md" + } + ] + }, + { + "name": "create-workflow", + "title": "Vapi Workflow Creation", + "description": "Build visual conversation workflows in Vapi with nodes for conversation steps, tool execution, conditional branching, and handoffs.", + "path": "create-workflow/SKILL.md", + "rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-workflow/SKILL.md", + "tags": ["workflows", "branching", "conversation-flow"], + "references": [] + } + ] +}