@vapi-ai/mcp-server 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +152 -6
  2. package/dist/knowledge/docs.d.ts +96 -0
  3. package/dist/knowledge/docs.d.ts.map +1 -0
  4. package/dist/knowledge/docs.js +16 -0
  5. package/dist/knowledge/skills.d.ts +28 -0
  6. package/dist/knowledge/skills.d.ts.map +1 -0
  7. package/dist/knowledge/skills.js +131 -0
  8. package/dist/schemas/assistant-openapi.d.ts +31 -0
  9. package/dist/schemas/assistant-openapi.d.ts.map +1 -0
  10. package/dist/schemas/assistant-openapi.js +87 -0
  11. package/dist/schemas/assistant-openapi.json +2341 -0
  12. package/dist/schemas/index.d.ts +158 -1181
  13. package/dist/schemas/index.d.ts.map +1 -1
  14. package/dist/schemas/index.js +95 -172
  15. package/dist/schemas/simulation-openapi.d.ts +9 -0
  16. package/dist/schemas/simulation-openapi.d.ts.map +1 -0
  17. package/dist/schemas/simulation-openapi.js +81 -0
  18. package/dist/schemas/simulation-openapi.json +27558 -0
  19. package/dist/schemas/simulation.d.ts +54 -0
  20. package/dist/schemas/simulation.d.ts.map +1 -0
  21. package/dist/schemas/simulation.js +76 -0
  22. package/dist/tools/api-schema.d.ts +4 -0
  23. package/dist/tools/api-schema.d.ts.map +1 -0
  24. package/dist/tools/api-schema.js +67 -0
  25. package/dist/tools/assistant.d.ts.map +1 -1
  26. package/dist/tools/assistant.js +25 -10
  27. package/dist/tools/call.d.ts.map +1 -1
  28. package/dist/tools/call.js +13 -6
  29. package/dist/tools/docs.d.ts +3 -0
  30. package/dist/tools/docs.d.ts.map +1 -0
  31. package/dist/tools/docs.js +27 -0
  32. package/dist/tools/index.d.ts.map +1 -1
  33. package/dist/tools/index.js +8 -0
  34. package/dist/tools/input-schema.d.ts +5 -0
  35. package/dist/tools/input-schema.d.ts.map +1 -0
  36. package/dist/tools/input-schema.js +59 -0
  37. package/dist/tools/phone-number.d.ts.map +1 -1
  38. package/dist/tools/phone-number.js +11 -5
  39. package/dist/tools/simulation.d.ts +4 -0
  40. package/dist/tools/simulation.d.ts.map +1 -0
  41. package/dist/tools/simulation.js +133 -0
  42. package/dist/tools/skills.d.ts +3 -0
  43. package/dist/tools/skills.d.ts.map +1 -0
  44. package/dist/tools/skills.js +31 -0
  45. package/dist/tools/tool.d.ts.map +1 -1
  46. package/dist/tools/tool.js +23 -9
  47. package/dist/tools/validation.d.ts +22 -0
  48. package/dist/tools/validation.d.ts.map +1 -0
  49. package/dist/tools/validation.js +94 -0
  50. package/dist/transformers/index.d.ts +3 -3
  51. package/dist/transformers/index.d.ts.map +1 -1
  52. package/dist/transformers/index.js +52 -31
  53. package/package.json +10 -7
package/README.md CHANGED
@@ -1,13 +1,7 @@
1
1
  # Vapi MCP Server
2
2
 
3
- [![smithery badge](https://smithery.ai/badge/@VapiAI/vapi-mcp-server)](https://smithery.ai/server/@VapiAI/vapi-mcp-server)
4
-
5
3
  Build AI voice assistants and phone agents with [Vapi](https://vapi.ai) using the [Model Context Protocol](https://modelcontextprotocol.com/).
6
4
 
7
- <a href="https://glama.ai/mcp/servers/@VapiAI/mcp-server">
8
- <img width="380" height="200" src="https://glama.ai/mcp/servers/@VapiAI/mcp-server/badge" alt="Vapi Server MCP server" />
9
- </a>
10
-
11
5
  ## Setup
12
6
 
13
7
  The MCP server requires a Vapi API key. Get one from the [Vapi dashboard](https://dashboard.vapi.ai/org/api-keys).
@@ -157,6 +151,24 @@ Connect to Vapi's hosted MCP server from any MCP client:
157
151
 
158
152
  ## Available Tools
159
153
 
154
+ ### Documentation
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `ask_vapi_docs` | Query the Fern docs MCP for current documentation passages and source URLs |
158
+
159
+ `ask_vapi_docs({"question":"How do assistant hooks work?","topK":8})` forwards to
160
+ `searchDocs` at `https://docs.vapi.ai/_mcp/server`. The result preserves Fern's
161
+ content, source links and error flag. This endpoint currently returns passages
162
+ for the calling agent to synthesize, rather than a generated answer. `topK`
163
+ defaults to 8 (maximum 20). Each request owns its MCP connection, supports caller
164
+ cancellation, times out after 30 seconds and closes the connection afterwards.
165
+
166
+ This tool needs outbound HTTPS to the Fern endpoint. It makes no Vapi API calls
167
+ and sends no Vapi credentials upstream. The server still requires `VAPI_TOKEN`
168
+ at startup. Discovery through `tools/list` makes no network requests; upstream
169
+ failures are returned as tool errors. No extra MCP connection is needed. Do not
170
+ include secrets or private customer data in docs questions.
171
+
160
172
  ### Assistants
161
173
  | Tool | Description |
162
174
  |------|-------------|
@@ -186,6 +198,55 @@ Connect to Vapi's hosted MCP server from any MCP client:
186
198
  | `create_tool` | Create tool for API integration |
187
199
  | `update_tool` | Update tool |
188
200
 
201
+ ### Public Skills
202
+ | Tool | Description |
203
+ |------|-------------|
204
+ | `search_skills` | Search public Vapi skills by keywords; omit `query` to browse |
205
+ | `load_skill` | Read a skill's `SKILL.md` or a supporting file |
206
+
207
+ For example, call `search_skills({"query":"simulations"})`, then
208
+ `load_skill({"name":"simulations"})`. The load result includes a `files` list;
209
+ read a supporting file with `load_skill({"name":"simulations","path":"references/example.md"})`,
210
+ using an exact path from that list. Loading returns instructions for the current
211
+ task; it does not install skills or run scripts.
212
+
213
+ Skills come directly from the public [VapiAI/skills](https://github.com/VapiAI/skills)
214
+ repository, including experimental skills. Discovery uses its GitHub tree rather
215
+ than the proposed skills manifest. Each server caches a commit-pinned snapshot
216
+ for five minutes, so file reads within that snapshot use the same revision. Search
217
+ matches names, descriptions and instruction text; it returns at most `limit`
218
+ results (default 10, maximum 30) and the total match count. `load_skill` returns
219
+ one text file at a time, up to 512 KB, with its source URL and revision.
220
+
221
+ The skills tools need outbound HTTPS to public GitHub. They make no Vapi API
222
+ calls and send no Vapi credentials upstream. The server still requires
223
+ `VAPI_TOKEN` at startup. Discovery through `tools/list` makes no network requests;
224
+ upstream failures are returned as tool errors. No GitHub token is needed for this
225
+ public content. GitHub's unauthenticated API rate limits apply.
226
+
227
+ ### Simulations
228
+
229
+ | Tool | Description |
230
+ |------|-------------|
231
+ | `list_simulation_personalities` | List simulated callers |
232
+ | `create_simulation_personality` | Create a simulated caller with a tester assistant |
233
+ | `list_simulation_scenarios` | List tester scripts and evaluations |
234
+ | `create_simulation_scenario` | Create a tester script and evaluations |
235
+ | `list_simulations` | List scenario/personality pairs |
236
+ | `create_simulation` | Pair a scenario with a personality |
237
+ | `list_simulation_suites` | List groups of simulations |
238
+ | `create_simulation_suite` | Group simulations into a suite |
239
+ | `run_simulations` | Run simulations or suites against an assistant or squad |
240
+ | `list_simulation_runs` | List runs and filter by status or target |
241
+ | `get_simulation_run` | Get run status and item summaries |
242
+ | `get_simulation_run_item` | Get a full item including transcript and results |
243
+
244
+ The catalog contains 29 tools. Simulation lists default to 50 results.
245
+ `get_simulation_run` returns the first 100 item summaries, with API pagination
246
+ metadata in `itemsMetadata` and `itemsTruncated: true` when more items exist.
247
+ Legacy array responses are also supported. Use `get_simulation_run_item` for
248
+ the full details of a known item.
249
+
189
250
  ---
190
251
 
191
252
  ## Development
@@ -247,6 +308,91 @@ export VAPI_TOKEN=your_test_org_token_here
247
308
  npm run test:live:mutating
248
309
  ```
249
310
 
311
+ ### Input validation from OpenAPI
312
+
313
+ All 21 parameterized MCP tools use imported OpenAPI constraints through
314
+ Zod's experimental `z.fromJSONSchema()`, pinned to Zod 4.6.7. This covers
315
+ assistant names/LLMs/voices/transcribers, call schedules and customer numbers,
316
+ lookup/update IDs, function parameters, transfer destinations, HTTP methods,
317
+ timeouts and retry plans, plus all 12 simulation tools. The four original list
318
+ tools have local empty input schemas.
319
+
320
+ `src/schemas/assistant-openapi.json` is the compact projection for the original
321
+ tool surfaces (the filename is retained from the initial assistant cutover). It
322
+ records the [public API spec](https://api.vapi.ai/api-json) source SHA-256 and
323
+ includes the recursive JSON-schema definitions needed for function parameters
324
+ and request bodies. Builds and installs use it without network access. Refresh
325
+ explicitly with `npm run schemas:snapshot`, or pass a saved spec with
326
+ `npm run schemas:snapshot -- /path/spec.json`.
327
+
328
+ `src/schemas/simulation-openapi.json` bundles the full simulation DTOs and their
329
+ referenced definitions from the same public spec. The same refresh command
330
+ updates both snapshots. Simulation validation includes names, folder paths,
331
+ UUIDs, instructions, evaluations, nested tester assistants and structured
332
+ outputs. Scenario/suite `additionalFields` are validated after merging, so they
333
+ cannot override an API constraint. Large nested assistant and structured-output
334
+ objects have compact advertised envelopes; their full API contracts are checked
335
+ at runtime. The shared summary projection omits long documentation and recursive
336
+ definitions from simulation discovery. The 25-tool catalog is about 28 KB.
337
+ No runtime schema fetch or
338
+ `get_api_schema` tool is required.
339
+
340
+ Simulation aliases remain local adapters: `run_simulations` takes exactly one
341
+ assistant/squad target and at least one simulation/suite ID, then builds the API
342
+ request. Integer iterations and integer page sizes of 1–100 are MCP policies;
343
+ API iteration bounds and parameter formats come from the snapshot. The item
344
+ lookup endpoint publishes plain strings for its IDs, so those are not assigned
345
+ UUID constraints from a different endpoint.
346
+
347
+ The shared normalizer also represents empty preset enums as always-failing
348
+ branches and preserves property names such as `oneOf` and `default` when walking
349
+ JSON Schema definitions.
350
+
351
+ When upgrading `@vapi-ai/server-sdk`, refresh and review the bundled schema in
352
+ the same PR, then run `npm run typecheck`, `npm test`, and `npm run test:package`.
353
+ Check changes against the SDK's request types and the MCP adapters below. The
354
+ public endpoint serves the current API spec, not a spec version tied to the SDK
355
+ release, so refreshing does not by itself guarantee compatibility. Keep the
356
+ reviewed schema bundled with the release; do not refresh it during installation
357
+ or startup.
358
+
359
+ The adapter documents published contract corrections: overlapping enum/string
360
+ `oneOf` alternatives become `anyOf`; duplicate enum values and JavaScript regex
361
+ delimiters are removed; the contradictory object/string-enum annotation on
362
+ `BackoffPlan.type` is corrected. Provider variants reject undeclared fields.
363
+ Spec defaults do not inject request fields; MCP defaults remain separate.
364
+ For the original compact assistant tools, variants requiring fields outside
365
+ the current interface remain excluded: custom
366
+ voice, Neuphonic voice, custom transcriber, Speechmatics transcriber, and custom
367
+ LLM. Adding their required fields needs an explicit interface extension.
368
+
369
+ MCP wrappers preserve field aliases and sparse updates. URL checks and
370
+ string-valued header maps remain explicit transport rules because the spec only
371
+ describes a string/object. API-request header maps and SMS metadata retain legacy MCP representations;
372
+ they are explicit compatibility adapters, not claimed to be full API DTO
373
+ validation. SMS metadata is not described in the published DTO. Response summaries also remain
374
+ local: they are transformed MCP representations, not raw API DTOs.
375
+
376
+ Validation failures return `isError: true`, concise text and structured
377
+ `validationError` details: tool name, issue code, field path, message and allowed
378
+ choices where relevant. Errors select the supplied provider's union branch,
379
+ include at most 12 issues and 8 allowed choices per issue, and indicate
380
+ truncation. Submitted values and credential contents are not echoed. Agents
381
+ should correct the listed fields and retry; a validation failure never invokes
382
+ the API handler. Tests exercise this correction loop through a real MCP client.
383
+
384
+ `tools/list` advertises a compact input summary derived from the same validators:
385
+ field names, basic types, required fields, defaults, and short constraints. It
386
+ omits provider/model catalogs, lengthy API documentation, and recursive schema
387
+ definitions. The full OpenAPI-derived validator still runs before any API call;
388
+ the advertised summary is intentionally broader. Catalog tests cap the
389
+ original 13-tool list at 20 KB, the 12 simulation tools at 10 KB, and the complete
390
+ 25-tool list at 30 KB to prevent unexpected agent-context growth.
391
+
392
+ `npm test` includes schema contract tests, independent Ajv parity cases, and
393
+ validation-feedback tests. The existing live compatibility
394
+ suites cover read-only resources and disposable assistants/custom tools.
395
+
250
396
  ---
251
397
 
252
398
  ## References
@@ -0,0 +1,96 @@
1
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2
+ export declare const DOCS_MCP_URL = "https://docs.vapi.ai/_mcp/server";
3
+ export declare function askVapiDocs(question: string, topK: number, signal?: AbortSignal, transport?: Transport): Promise<{
4
+ [x: string]: unknown;
5
+ content: ({
6
+ type: "text";
7
+ text: string;
8
+ annotations?: {
9
+ audience?: ("user" | "assistant")[] | undefined;
10
+ priority?: number | undefined;
11
+ lastModified?: string | undefined;
12
+ } | undefined;
13
+ _meta?: Record<string, unknown> | undefined;
14
+ } | {
15
+ type: "image";
16
+ data: string;
17
+ mimeType: string;
18
+ annotations?: {
19
+ audience?: ("user" | "assistant")[] | undefined;
20
+ priority?: number | undefined;
21
+ lastModified?: string | undefined;
22
+ } | undefined;
23
+ _meta?: Record<string, unknown> | undefined;
24
+ } | {
25
+ type: "audio";
26
+ data: string;
27
+ mimeType: string;
28
+ annotations?: {
29
+ audience?: ("user" | "assistant")[] | undefined;
30
+ priority?: number | undefined;
31
+ lastModified?: string | undefined;
32
+ } | undefined;
33
+ _meta?: Record<string, unknown> | undefined;
34
+ } | {
35
+ type: "resource";
36
+ resource: {
37
+ uri: string;
38
+ text: string;
39
+ mimeType?: string | undefined;
40
+ _meta?: Record<string, unknown> | undefined;
41
+ } | {
42
+ uri: string;
43
+ blob: string;
44
+ mimeType?: string | undefined;
45
+ _meta?: Record<string, unknown> | undefined;
46
+ };
47
+ annotations?: {
48
+ audience?: ("user" | "assistant")[] | undefined;
49
+ priority?: number | undefined;
50
+ lastModified?: string | undefined;
51
+ } | undefined;
52
+ _meta?: Record<string, unknown> | undefined;
53
+ } | {
54
+ uri: string;
55
+ name: string;
56
+ type: "resource_link";
57
+ description?: string | undefined;
58
+ mimeType?: string | undefined;
59
+ size?: number | undefined;
60
+ annotations?: {
61
+ audience?: ("user" | "assistant")[] | undefined;
62
+ priority?: number | undefined;
63
+ lastModified?: string | undefined;
64
+ } | undefined;
65
+ _meta?: {
66
+ [x: string]: unknown;
67
+ } | undefined;
68
+ icons?: {
69
+ src: string;
70
+ mimeType?: string | undefined;
71
+ sizes?: string[] | undefined;
72
+ theme?: "light" | "dark" | undefined;
73
+ }[] | undefined;
74
+ title?: string | undefined;
75
+ })[];
76
+ _meta?: {
77
+ [x: string]: unknown;
78
+ progressToken?: string | number | undefined;
79
+ "io.modelcontextprotocol/related-task"?: {
80
+ taskId: string;
81
+ } | undefined;
82
+ } | undefined;
83
+ structuredContent?: Record<string, unknown> | undefined;
84
+ isError?: boolean | undefined;
85
+ } | {
86
+ [x: string]: unknown;
87
+ toolResult: unknown;
88
+ _meta?: {
89
+ [x: string]: unknown;
90
+ progressToken?: string | number | undefined;
91
+ "io.modelcontextprotocol/related-task"?: {
92
+ taskId: string;
93
+ } | undefined;
94
+ } | undefined;
95
+ }>;
96
+ //# sourceMappingURL=docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/knowledge/docs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAE/E,eAAO,MAAM,YAAY,qCAAqC,CAAC;AAI/D,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,EACpB,SAAS,GAAE,SAAoE;;;;;mBAW44f,CAAC;oBAA4B,CAAC;oBAAgE,CAAC;wBAAkD,CAAC;;aAAkE,CAAC;;;;;mBAA+J,CAAC;oBAA4B,CAAC;oBAAgE,CAAC;wBAAkD,CAAC;;aAAkE,CAAC;;;;;mBAA+J,CAAC;oBAA4B,CAAC;oBAAgE,CAAC;wBAAkD,CAAC;;aAAkE,CAAC;;;;;;oBAA8L,CAAC;iBAA2C,CAAC;;;;oBAA4I,CAAC;iBAA2C,CAAC;;mBAA6E,CAAC;oBAA4B,CAAC;oBAAgE,CAAC;wBAAkD,CAAC;;aAAkE,CAAC;;;;;mBAAkK,CAAC;gBAA0C,CAAC;YAAsC,CAAC;mBAA6C,CAAC;oBAA4B,CAAC;oBAAgE,CAAC;wBAAkD,CAAC;;aAAkE,CAAC;;;aAAsF,CAAC;;oBAAyD,CAAC;iBAA2C,CAAC;iBAA6C,CAAC;;aAA8E,CAAC;;;;qBAAiH,CAAC;8CAAiF,CAAC;;;;;;;;;;;qBAAgV,CAAC;8CAAiF,CAAC;;;;GADv/lB"}
@@ -0,0 +1,16 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ export const DOCS_MCP_URL = 'https://docs.vapi.ai/_mcp/server';
4
+ // A separate connection per request avoids sharing a failed session or leaking
5
+ // concurrent calls across each other. No Vapi credentials are sent upstream.
6
+ export async function askVapiDocs(question, topK, signal, transport = new StreamableHTTPClientTransport(new URL(DOCS_MCP_URL))) {
7
+ const client = new Client({ name: 'vapi-mcp-docs', version: '1.0.0' });
8
+ const options = { signal: AbortSignal.any([AbortSignal.timeout(30_000), ...(signal ? [signal] : [])]), timeout: 30_000 };
9
+ try {
10
+ await client.connect(transport, options);
11
+ return await client.callTool({ name: 'searchDocs', arguments: { query: question, topK } }, undefined, options);
12
+ }
13
+ finally {
14
+ await client.close().catch(() => undefined);
15
+ }
16
+ }
@@ -0,0 +1,28 @@
1
+ export declare class PublicSkills {
2
+ private readonly fetcher;
3
+ private cached?;
4
+ constructor(fetcher?: typeof fetch);
5
+ private read;
6
+ private snapshot;
7
+ private content;
8
+ private skill;
9
+ search(query: string, limit: number): Promise<{
10
+ repository: string;
11
+ revision: string;
12
+ total: number;
13
+ skills: {
14
+ name: string;
15
+ description: string;
16
+ sourceUrl: string;
17
+ }[];
18
+ }>;
19
+ load(name: string, path: string): Promise<{
20
+ name: string;
21
+ path: string;
22
+ revision: string;
23
+ sourceUrl: string;
24
+ content: string;
25
+ files: string[];
26
+ }>;
27
+ }
28
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/knowledge/skills.ts"],"names":[],"mappings":"AAwBA,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,MAAM,CAAC,CAAoD;gBAEtC,OAAO,GAAE,OAAO,KAAa;YAE5C,IAAI;IA6BlB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,OAAO;YAWD,KAAK;IAQb,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;IAyBnC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;;;;;;;;CAatC"}
@@ -0,0 +1,131 @@
1
+ import { parse } from 'yaml';
2
+ import { z } from 'zod';
3
+ const REPOSITORY = 'https://github.com/VapiAI/skills';
4
+ const API = 'https://api.github.com/repos/VapiAI/skills';
5
+ const RAW = 'https://raw.githubusercontent.com/VapiAI/skills';
6
+ const CACHE_MS = 5 * 60_000;
7
+ const TEXT_FILE = /\.(md|txt|json|ya?ml|py|[cm]?js|ts|sh|rst|toml)$/i;
8
+ const commitSchema = z.object({ sha: z.string().regex(/^[a-f0-9]{40}$/) });
9
+ const treeSchema = z.object({
10
+ truncated: z.literal(false),
11
+ tree: z.array(z.object({ path: z.string(), type: z.string(), mode: z.string() })),
12
+ });
13
+ // Each MCP server has its own bounded-lifetime snapshot. Rejected fetches are
14
+ // evicted so an upstream outage doesn't poison later calls.
15
+ export class PublicSkills {
16
+ fetcher;
17
+ cached;
18
+ constructor(fetcher = fetch) {
19
+ this.fetcher = fetcher;
20
+ }
21
+ async read(url, maxBytes = 512_000) {
22
+ const response = await this.fetcher(url, {
23
+ signal: AbortSignal.timeout(15_000),
24
+ redirect: 'error',
25
+ headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'vapi-mcp-server' },
26
+ });
27
+ if (!response.ok) {
28
+ await response.body?.cancel();
29
+ throw new Error(`Public skills request failed (HTTP ${response.status}). Retry later.`);
30
+ }
31
+ if (!response.body)
32
+ throw new Error('Public skills returned an empty response.');
33
+ const reader = response.body.getReader();
34
+ const chunks = [];
35
+ let size = 0;
36
+ try {
37
+ while (true) {
38
+ const { done, value } = await reader.read();
39
+ if (done)
40
+ break;
41
+ size += value.byteLength;
42
+ if (size > maxBytes)
43
+ throw new Error('Public skills response exceeds the supported size.');
44
+ chunks.push(value);
45
+ }
46
+ }
47
+ finally {
48
+ await reader.cancel().catch(() => undefined);
49
+ reader.releaseLock();
50
+ }
51
+ return Buffer.concat(chunks).toString('utf8');
52
+ }
53
+ snapshot() {
54
+ if (this.cached && this.cached.expiresAt > Date.now())
55
+ return this.cached.promise;
56
+ const promise = (async () => {
57
+ // Pin file reads to one commit so SKILL.md and its references agree even
58
+ // when the public repository changes between requests.
59
+ const { sha } = commitSchema.parse(JSON.parse(await this.read(`${API}/commits/main`)));
60
+ const tree = treeSchema.parse(JSON.parse(await this.read(`${API}/git/trees/${sha}?recursive=1`, 2_000_000)));
61
+ const files = new Set(tree.tree.filter(entry => entry.type === 'blob' && ['100644', '100755'].includes(entry.mode)).map(entry => entry.path));
62
+ const names = [...files].filter(path => /^[a-z0-9]+(?:-[a-z0-9]+)*\/SKILL\.md$/.test(path)).map(path => path.split('/')[0]).sort();
63
+ if (!names.length)
64
+ throw new Error('No public Vapi skills were found. Retry later.');
65
+ return { revision: sha, files, names, contents: new Map() };
66
+ })();
67
+ this.cached = { expiresAt: Date.now() + CACHE_MS, promise };
68
+ void promise.catch(() => { if (this.cached?.promise === promise)
69
+ this.cached = undefined; });
70
+ return promise;
71
+ }
72
+ content(snapshot, path) {
73
+ const cached = snapshot.contents.get(path);
74
+ if (cached)
75
+ return cached;
76
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
77
+ const promise = this.read(`${RAW}/${snapshot.revision}/${encodedPath}`);
78
+ if (snapshot.contents.size >= 100)
79
+ snapshot.contents.delete(snapshot.contents.keys().next().value);
80
+ snapshot.contents.set(path, promise);
81
+ void promise.catch(() => snapshot.contents.delete(path));
82
+ return promise;
83
+ }
84
+ async skill(snapshot, name) {
85
+ const content = await this.content(snapshot, `${name}/SKILL.md`);
86
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
87
+ if (!frontmatter)
88
+ throw new Error(`Invalid frontmatter for public skill ${name}.`);
89
+ const metadata = z.object({ name: z.literal(name), description: z.string().min(1) }).parse(parse(frontmatter[1]));
90
+ return { name, description: metadata.description, content };
91
+ }
92
+ async search(query, limit) {
93
+ const snapshot = await this.snapshot();
94
+ const skills = [];
95
+ // Limit concurrent public requests when warming a new snapshot.
96
+ for (let index = 0; index < snapshot.names.length; index += 4) {
97
+ skills.push(...await Promise.all(snapshot.names.slice(index, index + 4).map(name => this.skill(snapshot, name))));
98
+ }
99
+ const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])];
100
+ const matches = skills.map(skill => ({
101
+ skill,
102
+ score: terms.reduce((score, term) => score +
103
+ (skill.name.includes(term) ? 5 : 0) +
104
+ (skill.description.toLowerCase().includes(term) ? 3 : 0) +
105
+ (skill.content.toLowerCase().includes(term) ? 1 : 0), 0),
106
+ })).filter(({ score }) => !terms.length || score > 0)
107
+ .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
108
+ return {
109
+ repository: REPOSITORY, revision: snapshot.revision, total: matches.length,
110
+ skills: matches.slice(0, limit).map(({ skill }) => ({
111
+ name: skill.name, description: skill.description,
112
+ sourceUrl: `${REPOSITORY}/blob/${snapshot.revision}/${skill.name}/SKILL.md`,
113
+ })),
114
+ };
115
+ }
116
+ async load(name, path) {
117
+ const snapshot = await this.snapshot();
118
+ if (!snapshot.names.includes(name))
119
+ throw new Error(`Unknown public skill. Use search_skills to find a skill name.`);
120
+ const files = [...snapshot.files].filter(file => file.startsWith(`${name}/`) && TEXT_FILE.test(file)).map(file => file.slice(name.length + 1)).sort();
121
+ // Exact tree membership, not URL/path normalization, bounds reads to this
122
+ // skill and excludes traversal, URLs, symlinks and binary assets.
123
+ if (!files.includes(path))
124
+ throw new Error('Unknown skill file. Load SKILL.md to see the available files.');
125
+ return {
126
+ name, path, revision: snapshot.revision,
127
+ sourceUrl: `${REPOSITORY}/blob/${snapshot.revision}/${name}/${path}`,
128
+ content: await this.content(snapshot, `${name}/${path}`), files,
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ type JsonSchema = z.core.JSONSchema.JSONSchema;
3
+ export declare const requiredToolConfigurations: string[];
4
+ export declare const functionRequiresName: boolean;
5
+ export declare function inputContract(value: unknown, schemaMap?: boolean): unknown;
6
+ export declare function providerContract(field: 'voice' | 'transcriber' | 'llm'): JsonSchema;
7
+ export declare const providersRequiringExpandedInputs: {
8
+ [k: string]: string[];
9
+ };
10
+ type VoiceConfiguration = {
11
+ provider: string;
12
+ voiceId: string;
13
+ model?: string;
14
+ };
15
+ type TranscriberConfiguration = {
16
+ provider: string;
17
+ model?: string;
18
+ };
19
+ export declare const VoiceInputSchema: z.ZodType<VoiceConfiguration>;
20
+ export declare const TranscriberInputSchema: z.ZodType<TranscriberConfiguration>;
21
+ export declare const CreateAssistantNameSchema: z.ZodType<string>;
22
+ export declare const UpdateAssistantNameSchema: z.ZodType<string>;
23
+ export declare const LLMInputSchema: z.ZodType<{
24
+ provider: string;
25
+ model?: string;
26
+ }>;
27
+ export declare function fieldContract(name: string): JsonSchema;
28
+ export declare function apiField<T = unknown>(name: string, overrides?: Partial<JsonSchema>): z.ZodType<T>;
29
+ export declare function apiObjectField<T = Record<string, unknown>>(name: string): z.ZodType<T>;
30
+ export {};
31
+ //# sourceMappingURL=assistant-openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-openapi.d.ts","sourceRoot":"","sources":["../../src/schemas/assistant-openapi.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,KAAK,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;AAkB/C,eAAO,MAAM,0BAA0B,UAAsC,CAAC;AAC9E,eAAO,MAAM,oBAAoB,SAAgC,CAAC;AAKlE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,UAAQ,GAAG,OAAO,CAiCxE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,KAAK,GAAG,UAAU,CAanF;AAID,eAAO,MAAM,gCAAgC;;CAI5C,CAAC;AAEF,KAAK,kBAAkB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAChF,KAAK,wBAAwB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAIrE,eAAO,MAAM,gBAAgB,EAAoF,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAC/I,eAAO,MAAM,sBAAsB,EAA0F,CAAC,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;AACjK,eAAO,MAAM,yBAAyB,EAA+E,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACvI,eAAO,MAAM,yBAAyB,EAA+E,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAEvI,eAAO,MAAM,cAAc,EAAkF,CAAC,CAAC,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7J,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAKtD;AAED,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,CAAC,UAAU,CAAM,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAErG;AAED,wBAAgB,cAAc,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAKtF"}
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ const snapshot = JSON.parse(readFileSync(new URL('./assistant-openapi.json', import.meta.url), 'utf8'));
4
+ export const requiredToolConfigurations = snapshot.requiredToolConfigurations;
5
+ export const functionRequiresName = snapshot.functionRequiresName;
6
+ // The published spec uses overlapping oneOf [enum, string] branches for preset
7
+ // IDs and model names. The API accepts both. Change only this recognizable
8
+ // pattern, leaving discriminated/exclusive unions intact.
9
+ export function inputContract(value, schemaMap = false) {
10
+ if (Array.isArray(value))
11
+ return value.map(item => inputContract(item));
12
+ if (!value || typeof value !== 'object')
13
+ return value;
14
+ const schema = value;
15
+ // Property names such as oneOf/default are user-defined keys in schema maps,
16
+ // rather than JSON Schema keywords. Preserve them and normalize their values.
17
+ if (schemaMap)
18
+ return Object.fromEntries(Object.entries(schema).map(([key, nested]) => [key, inputContract(nested)]));
19
+ // An empty published preset enum denotes no preset values. Represent that
20
+ // branch as an always-failing schema so it remains valid JSON Schema.
21
+ if (Array.isArray(schema.enum) && schema.enum.length === 0)
22
+ return { not: {} };
23
+ const alternatives = Array.isArray(schema.oneOf) ? schema.oneOf : undefined;
24
+ const overlap = alternatives?.some(branch => branch.type === 'string' &&
25
+ Object.keys(branch).every(key => ['type', 'title', 'description'].includes(key))) &&
26
+ alternatives.some(branch => branch.type === 'string' && Array.isArray(branch.enum));
27
+ return Object.fromEntries(Object.entries(schema)
28
+ // OpenAPI defaults are annotations; preserve omitted input fields and let
29
+ // the API apply its defaults rather than materializing them in requests.
30
+ .filter(([key]) => key !== 'default')
31
+ .map(([key, nested]) => [
32
+ key === 'oneOf' && overlap ? 'anyOf' : key,
33
+ // Some published voice IDs repeat in enum arrays. Deduplication changes
34
+ // no accepted values and keeps the result valid JSON Schema for Ajv.
35
+ key === 'enum' && Array.isArray(nested) ? [...new Set(nested)]
36
+ // Published name patterns include JavaScript /.../ delimiters, while
37
+ // JSON Schema pattern expects the regex source without delimiters.
38
+ : key === 'pattern' && typeof nested === 'string' && nested.startsWith('/') && nested.endsWith('/') ? nested.slice(1, -1)
39
+ // BackoffPlan.type is published as object with a string enum. Keep the
40
+ // enum's actual value type; the incompatible type annotation is a spec defect.
41
+ : key === 'type' && nested === 'object' && Array.isArray(schema.enum) && schema.enum.every(v => typeof v === 'string') ? 'string'
42
+ : ['properties', 'definitions', '$defs', 'patternProperties', 'dependentSchemas'].includes(key) ? inputContract(nested, true)
43
+ : ['allOf', 'anyOf', 'oneOf', 'not', 'items', 'additionalProperties', 'contains', 'if', 'then', 'else'].includes(key) ? inputContract(nested)
44
+ : nested,
45
+ ]));
46
+ }
47
+ export function providerContract(field) {
48
+ const supported = snapshot[field].filter(definition => definition.required.every(key => key in definition.properties));
49
+ return {
50
+ oneOf: supported.map(definition => ({
51
+ type: 'object',
52
+ required: definition.required,
53
+ properties: inputContract(definition.properties, true),
54
+ // API DTOs reject undeclared provider fields. In particular, a provider
55
+ // without a model property must reject model, not silently accept it.
56
+ additionalProperties: false,
57
+ })),
58
+ };
59
+ }
60
+ // Providers requiring fields outside the existing compact MCP surface stay
61
+ // unsupported, rather than having those API-required fields silently dropped.
62
+ export const providersRequiringExpandedInputs = Object.fromEntries(['voice', 'transcriber', 'llm'].map(field => [field, snapshot[field]
63
+ .filter(definition => definition.required.some(key => !(key in definition.properties)))
64
+ .map(definition => definition.name)]));
65
+ // fromJSONSchema validates at runtime but intentionally returns an untyped
66
+ // ZodType. These types describe the projected fields, not a second validator.
67
+ export const VoiceInputSchema = z.fromJSONSchema(providerContract('voice'), { defaultTarget: 'openapi-3.0' });
68
+ export const TranscriberInputSchema = z.fromJSONSchema(providerContract('transcriber'), { defaultTarget: 'openapi-3.0' });
69
+ export const CreateAssistantNameSchema = z.fromJSONSchema(snapshot.name.create, { defaultTarget: 'openapi-3.0' });
70
+ export const UpdateAssistantNameSchema = z.fromJSONSchema(snapshot.name.update, { defaultTarget: 'openapi-3.0' });
71
+ export const LLMInputSchema = z.fromJSONSchema(providerContract('llm'), { defaultTarget: 'openapi-3.0' });
72
+ export function fieldContract(name) {
73
+ const field = snapshot.fields[name];
74
+ if (!field)
75
+ throw new Error(`Unknown API input field: ${name}`);
76
+ return JSON.parse(JSON.stringify(inputContract({ ...field, definitions: snapshot.definitions }), (key, value) => key === '$ref' && typeof value === 'string' ? value.replace('#/components/schemas/', '#/definitions/') : value));
77
+ }
78
+ export function apiField(name, overrides = {}) {
79
+ return z.fromJSONSchema({ ...fieldContract(name), ...overrides }, { defaultTarget: 'openapi-3.0' });
80
+ }
81
+ export function apiObjectField(name) {
82
+ const schema = apiField(name);
83
+ if (!(schema instanceof z.ZodObject))
84
+ throw new Error(`${name} is no longer an object schema`);
85
+ // Preserve the existing MCP policy of stripping unknown envelope fields.
86
+ return schema.strip();
87
+ }