Skip to content

Commit 03824bd

Browse files
committed
Add machine-readable skills manifest
1 parent 9b95b89 commit 03824bd

4 files changed

Lines changed: 353 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ claude mcp add vapi-docs -- npx -y mcp-remote https://docs.vapi.ai/_mcp/server
8585
| [setup-webhook](./setup-webhook) | Configure server URLs to receive real-time call events |
8686
| [create-workflow](./create-workflow) | Build visual conversation workflows with branching logic |
8787

88+
## Machine-Readable Manifest
89+
90+
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.
91+
92+
Validate manifest changes with:
93+
94+
```bash
95+
node scripts/validate-skills-manifest.mjs
96+
```
97+
8898
## Configuration
8999

90100
All skills require a Vapi API key. Set it as an environment variable:
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://raw.githubusercontent.com/VapiAI/skills/main/schemas/skills.manifest.schema.json",
4+
"title": "Vapi Skills Manifest",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": ["schemaVersion", "name", "description", "repository", "skills"],
8+
"properties": {
9+
"$schema": {
10+
"type": "string"
11+
},
12+
"schemaVersion": {
13+
"type": "string"
14+
},
15+
"name": {
16+
"type": "string",
17+
"minLength": 1
18+
},
19+
"description": {
20+
"type": "string",
21+
"minLength": 1
22+
},
23+
"repository": {
24+
"type": "object",
25+
"additionalProperties": false,
26+
"required": ["owner", "name", "defaultBranch", "htmlUrl", "rawBaseUrl"],
27+
"properties": {
28+
"owner": {
29+
"type": "string",
30+
"minLength": 1
31+
},
32+
"name": {
33+
"type": "string",
34+
"minLength": 1
35+
},
36+
"defaultBranch": {
37+
"type": "string",
38+
"minLength": 1
39+
},
40+
"htmlUrl": {
41+
"type": "string",
42+
"format": "uri"
43+
},
44+
"rawBaseUrl": {
45+
"type": "string",
46+
"format": "uri"
47+
}
48+
}
49+
},
50+
"skills": {
51+
"type": "array",
52+
"minItems": 1,
53+
"items": {
54+
"$ref": "#/$defs/skill"
55+
}
56+
}
57+
},
58+
"$defs": {
59+
"path": {
60+
"type": "string",
61+
"pattern": "^[A-Za-z0-9._/-]+$",
62+
"not": {
63+
"anyOf": [
64+
{ "pattern": "^/" },
65+
{ "pattern": "(^|/)\\.\\.(/|$)" }
66+
]
67+
}
68+
},
69+
"reference": {
70+
"type": "object",
71+
"additionalProperties": false,
72+
"required": ["name", "path", "rawUrl"],
73+
"properties": {
74+
"name": {
75+
"type": "string",
76+
"minLength": 1
77+
},
78+
"path": {
79+
"$ref": "#/$defs/path"
80+
},
81+
"rawUrl": {
82+
"type": "string",
83+
"format": "uri"
84+
}
85+
}
86+
},
87+
"skill": {
88+
"type": "object",
89+
"additionalProperties": false,
90+
"required": ["name", "title", "description", "path", "rawUrl", "tags", "references"],
91+
"properties": {
92+
"name": {
93+
"type": "string",
94+
"pattern": "^[a-z0-9][a-z0-9-]*$"
95+
},
96+
"title": {
97+
"type": "string",
98+
"minLength": 1
99+
},
100+
"description": {
101+
"type": "string",
102+
"minLength": 1
103+
},
104+
"path": {
105+
"$ref": "#/$defs/path"
106+
},
107+
"rawUrl": {
108+
"type": "string",
109+
"format": "uri"
110+
},
111+
"tags": {
112+
"type": "array",
113+
"items": {
114+
"type": "string",
115+
"pattern": "^[a-z0-9][a-z0-9-]*$"
116+
}
117+
},
118+
"references": {
119+
"type": "array",
120+
"items": {
121+
"$ref": "#/$defs/reference"
122+
}
123+
}
124+
}
125+
}
126+
}
127+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import path from 'node:path';
3+
import process from 'node:process';
4+
5+
const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
6+
const manifestPath = path.join(repoRoot, 'skills.manifest.json');
7+
const marketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json');
8+
9+
const fail = (message) => {
10+
console.error(message);
11+
process.exitCode = 1;
12+
};
13+
14+
const readJson = (filePath) => JSON.parse(readFileSync(filePath, 'utf8'));
15+
16+
const isSafePath = (filePath) =>
17+
typeof filePath === 'string' &&
18+
filePath.length > 0 &&
19+
!path.isAbsolute(filePath) &&
20+
!filePath.split('/').includes('..');
21+
22+
const skillFrontmatterGet = (filePath) => {
23+
const content = readFileSync(filePath, 'utf8');
24+
const match = content.match(/^---\n([\s\S]*?)\n---/);
25+
if (!match) {
26+
return {};
27+
}
28+
29+
return Object.fromEntries(
30+
match[1]
31+
.split('\n')
32+
.map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/))
33+
.filter(Boolean)
34+
.map(([, key, value]) => [key, value.replace(/^"|"$/g, '')]),
35+
);
36+
};
37+
38+
const manifest = readJson(manifestPath);
39+
const marketplace = readJson(marketplacePath);
40+
const marketplaceSkills = new Set(
41+
marketplace.plugins.flatMap((plugin) =>
42+
plugin.skills.map((skillPath) => skillPath.replace(/^\.\//, '')),
43+
),
44+
);
45+
const manifestNames = new Set();
46+
47+
if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) {
48+
fail('skills.manifest.json must include at least one skill');
49+
}
50+
51+
for (const skill of manifest.skills ?? []) {
52+
if (!skill.name || manifestNames.has(skill.name)) {
53+
fail(`duplicate or missing skill name: ${skill.name ?? '<missing>'}`);
54+
continue;
55+
}
56+
manifestNames.add(skill.name);
57+
58+
if (!marketplaceSkills.has(skill.name)) {
59+
fail(`${skill.name} is missing from .claude-plugin/marketplace.json`);
60+
}
61+
62+
if (!isSafePath(skill.path)) {
63+
fail(`${skill.name} has an unsafe path: ${skill.path}`);
64+
continue;
65+
}
66+
67+
const localPath = path.join(repoRoot, skill.path);
68+
if (!existsSync(localPath)) {
69+
fail(`${skill.name} path does not exist: ${skill.path}`);
70+
continue;
71+
}
72+
73+
const frontmatter = skillFrontmatterGet(localPath);
74+
if (frontmatter.name !== skill.name) {
75+
fail(`${skill.name} frontmatter name mismatch: ${frontmatter.name}`);
76+
}
77+
78+
if (!skill.rawUrl?.endsWith(skill.path)) {
79+
fail(`${skill.name} rawUrl must end with ${skill.path}`);
80+
}
81+
82+
for (const reference of skill.references ?? []) {
83+
if (!isSafePath(reference.path)) {
84+
fail(`${skill.name} has an unsafe reference path: ${reference.path}`);
85+
continue;
86+
}
87+
88+
if (!existsSync(path.join(repoRoot, reference.path))) {
89+
fail(`${skill.name} reference does not exist: ${reference.path}`);
90+
}
91+
92+
if (!reference.rawUrl?.endsWith(reference.path)) {
93+
fail(`${skill.name} reference rawUrl must end with ${reference.path}`);
94+
}
95+
}
96+
}
97+
98+
for (const marketplaceSkill of marketplaceSkills) {
99+
if (!manifestNames.has(marketplaceSkill)) {
100+
fail(`${marketplaceSkill} is missing from skills.manifest.json`);
101+
}
102+
}
103+
104+
if (!process.exitCode) {
105+
console.log(`Validated ${manifest.skills.length} Vapi skills`);
106+
}

skills.manifest.json

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"$schema": "./schemas/skills.manifest.schema.json",
3+
"schemaVersion": "2026-05-04",
4+
"name": "vapi-skills",
5+
"description": "Agent skills for building voice AI agents with Vapi.",
6+
"repository": {
7+
"owner": "VapiAI",
8+
"name": "skills",
9+
"defaultBranch": "main",
10+
"htmlUrl": "https://github.com/VapiAI/skills",
11+
"rawBaseUrl": "https://raw.githubusercontent.com/VapiAI/skills/main"
12+
},
13+
"skills": [
14+
{
15+
"name": "setup-api-key",
16+
"title": "Vapi API Key Setup",
17+
"description": "Guide users through obtaining and configuring a Vapi API key.",
18+
"path": "setup-api-key/SKILL.md",
19+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-api-key/SKILL.md",
20+
"tags": ["setup", "auth", "api-key"],
21+
"references": []
22+
},
23+
{
24+
"name": "create-assistant",
25+
"title": "Vapi Assistant Creation",
26+
"description": "Create and configure Vapi voice AI assistants with models, voices, transcribers, tools, hooks, and advanced settings.",
27+
"path": "create-assistant/SKILL.md",
28+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/SKILL.md",
29+
"tags": ["assistants", "voice-agent", "configuration"],
30+
"references": [
31+
{
32+
"name": "hooks",
33+
"path": "create-assistant/references/hooks.md",
34+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/references/hooks.md"
35+
},
36+
{
37+
"name": "providers",
38+
"path": "create-assistant/references/providers.md",
39+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-assistant/references/providers.md"
40+
}
41+
]
42+
},
43+
{
44+
"name": "create-tool",
45+
"title": "Vapi Tool Creation",
46+
"description": "Create custom tools for Vapi voice assistants including function tools, API request tools, transfer call tools, end call tools, and integrations.",
47+
"path": "create-tool/SKILL.md",
48+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-tool/SKILL.md",
49+
"tags": ["tools", "integrations", "function-calling"],
50+
"references": [
51+
{
52+
"name": "tool-server",
53+
"path": "create-tool/references/tool-server.md",
54+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-tool/references/tool-server.md"
55+
}
56+
]
57+
},
58+
{
59+
"name": "create-call",
60+
"title": "Vapi Call Creation",
61+
"description": "Create outbound phone calls, web calls, and batch calls using the Vapi API.",
62+
"path": "create-call/SKILL.md",
63+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-call/SKILL.md",
64+
"tags": ["calls", "outbound", "testing"],
65+
"references": []
66+
},
67+
{
68+
"name": "create-squad",
69+
"title": "Vapi Squad Creation",
70+
"description": "Create multi-assistant squads in Vapi with handoffs between specialized voice agents.",
71+
"path": "create-squad/SKILL.md",
72+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-squad/SKILL.md",
73+
"tags": ["squads", "handoffs", "multi-agent"],
74+
"references": []
75+
},
76+
{
77+
"name": "create-phone-number",
78+
"title": "Vapi Phone Number Setup",
79+
"description": "Set up and manage phone numbers in Vapi for inbound and outbound voice AI calls.",
80+
"path": "create-phone-number/SKILL.md",
81+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-phone-number/SKILL.md",
82+
"tags": ["phone-numbers", "telephony", "providers"],
83+
"references": []
84+
},
85+
{
86+
"name": "setup-webhook",
87+
"title": "Vapi Webhook / Server URL Setup",
88+
"description": "Configure Vapi server URLs and webhooks to receive real-time call events, transcripts, tool calls, and end-of-call reports.",
89+
"path": "setup-webhook/SKILL.md",
90+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-webhook/SKILL.md",
91+
"tags": ["webhooks", "server-url", "events"],
92+
"references": [
93+
{
94+
"name": "webhook-events",
95+
"path": "setup-webhook/references/webhook-events.md",
96+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/setup-webhook/references/webhook-events.md"
97+
}
98+
]
99+
},
100+
{
101+
"name": "create-workflow",
102+
"title": "Vapi Workflow Creation",
103+
"description": "Build visual conversation workflows in Vapi with nodes for conversation steps, tool execution, conditional branching, and handoffs.",
104+
"path": "create-workflow/SKILL.md",
105+
"rawUrl": "https://raw.githubusercontent.com/VapiAI/skills/main/create-workflow/SKILL.md",
106+
"tags": ["workflows", "branching", "conversation-flow"],
107+
"references": []
108+
}
109+
]
110+
}

0 commit comments

Comments
 (0)