Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
127 changes: 127 additions & 0 deletions schemas/skills.manifest.schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
106 changes: 106 additions & 0 deletions scripts/validate-skills-manifest.mjs
Original file line number Diff line number Diff line change
@@ -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 ?? '<missing>'}`);
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`);
}
110 changes: 110 additions & 0 deletions skills.manifest.json
Original file line number Diff line number Diff line change
@@ -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": []
}
]
}