@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.
- package/README.md +152 -6
- package/dist/knowledge/docs.d.ts +96 -0
- package/dist/knowledge/docs.d.ts.map +1 -0
- package/dist/knowledge/docs.js +16 -0
- package/dist/knowledge/skills.d.ts +28 -0
- package/dist/knowledge/skills.d.ts.map +1 -0
- package/dist/knowledge/skills.js +131 -0
- package/dist/schemas/assistant-openapi.d.ts +31 -0
- package/dist/schemas/assistant-openapi.d.ts.map +1 -0
- package/dist/schemas/assistant-openapi.js +87 -0
- package/dist/schemas/assistant-openapi.json +2341 -0
- package/dist/schemas/index.d.ts +158 -1181
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +95 -172
- package/dist/schemas/simulation-openapi.d.ts +9 -0
- package/dist/schemas/simulation-openapi.d.ts.map +1 -0
- package/dist/schemas/simulation-openapi.js +81 -0
- package/dist/schemas/simulation-openapi.json +27558 -0
- package/dist/schemas/simulation.d.ts +54 -0
- package/dist/schemas/simulation.d.ts.map +1 -0
- package/dist/schemas/simulation.js +76 -0
- package/dist/tools/api-schema.d.ts +4 -0
- package/dist/tools/api-schema.d.ts.map +1 -0
- package/dist/tools/api-schema.js +67 -0
- package/dist/tools/assistant.d.ts.map +1 -1
- package/dist/tools/assistant.js +25 -10
- package/dist/tools/call.d.ts.map +1 -1
- package/dist/tools/call.js +13 -6
- package/dist/tools/docs.d.ts +3 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/docs.js +27 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/input-schema.d.ts +5 -0
- package/dist/tools/input-schema.d.ts.map +1 -0
- package/dist/tools/input-schema.js +59 -0
- package/dist/tools/phone-number.d.ts.map +1 -1
- package/dist/tools/phone-number.js +11 -5
- package/dist/tools/simulation.d.ts +4 -0
- package/dist/tools/simulation.d.ts.map +1 -0
- package/dist/tools/simulation.js +133 -0
- package/dist/tools/skills.d.ts +3 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +31 -0
- package/dist/tools/tool.d.ts.map +1 -1
- package/dist/tools/tool.js +23 -9
- package/dist/tools/validation.d.ts +22 -0
- package/dist/tools/validation.d.ts.map +1 -0
- package/dist/tools/validation.js +94 -0
- package/dist/transformers/index.d.ts +3 -3
- package/dist/transformers/index.d.ts.map +1 -1
- package/dist/transformers/index.js +52 -31
- package/package.json +10 -7
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CreateSimulationInputSchema, CreateSimulationPersonalityInputSchema, CreateSimulationScenarioInputSchema, CreateSimulationSuiteInputSchema, GetSimulationRunInputSchema, GetSimulationRunItemInputSchema, ListSimulationResourcesInputSchema, ListSimulationRunsInputSchema, ListSimulationScenariosInputSchema, RunSimulationsInputSchema, } from '../schemas/index.js';
|
|
2
|
+
import { registerValidatedTool } from './validation.js';
|
|
3
|
+
import { createToolHandler } from './utils.js';
|
|
4
|
+
const DEFAULT_LIMIT = 50;
|
|
5
|
+
// Run items carry call snapshots and transcripts in these fields, which can
|
|
6
|
+
// be large. get_simulation_run leaves them out; get_simulation_run_item
|
|
7
|
+
// returns them.
|
|
8
|
+
const LARGE_RUN_ITEM_FIELDS = [
|
|
9
|
+
'metadata',
|
|
10
|
+
'configurations',
|
|
11
|
+
'improvementSuggestions',
|
|
12
|
+
];
|
|
13
|
+
function summarizeRunItem(item) {
|
|
14
|
+
const summary = { ...item };
|
|
15
|
+
for (const field of LARGE_RUN_ITEM_FIELDS) {
|
|
16
|
+
delete summary[field];
|
|
17
|
+
}
|
|
18
|
+
return summary;
|
|
19
|
+
}
|
|
20
|
+
export const registerSimulationTools = (server, vapiClient) => {
|
|
21
|
+
registerValidatedTool(server, 'list_simulation_personalities', {
|
|
22
|
+
description: 'Lists simulation personalities: the simulated callers (testers) used in Vapi simulations',
|
|
23
|
+
inputSchema: ListSimulationResourcesInputSchema,
|
|
24
|
+
}, createToolHandler(async (data) => vapiClient.simulationPersonalities.personalityControllerFindAll({
|
|
25
|
+
limit: data.limit ?? DEFAULT_LIMIT,
|
|
26
|
+
})));
|
|
27
|
+
registerValidatedTool(server, 'create_simulation_personality', {
|
|
28
|
+
description: 'Creates a simulation personality: the simulated caller that talks to the assistant under test',
|
|
29
|
+
inputSchema: CreateSimulationPersonalityInputSchema,
|
|
30
|
+
}, createToolHandler(async (data) => vapiClient.simulationPersonalities.personalityControllerCreate(data)));
|
|
31
|
+
registerValidatedTool(server, 'list_simulation_scenarios', {
|
|
32
|
+
description: 'Lists simulation scenarios: the scripts testers follow and the checks that decide pass or fail',
|
|
33
|
+
inputSchema: ListSimulationScenariosInputSchema,
|
|
34
|
+
}, createToolHandler(async (data) => vapiClient.simulationScenarios.scenarioControllerFindAll({
|
|
35
|
+
limit: data.limit ?? DEFAULT_LIMIT,
|
|
36
|
+
...(data.name && { name: data.name }),
|
|
37
|
+
})));
|
|
38
|
+
registerValidatedTool(server, 'create_simulation_scenario', {
|
|
39
|
+
description: 'Creates a simulation scenario: instructions for the tester plus evaluations that decide whether the assistant passed',
|
|
40
|
+
inputSchema: CreateSimulationScenarioInputSchema,
|
|
41
|
+
}, createToolHandler(async ({ additionalFields, ...data }) => vapiClient.simulationScenarios.scenarioControllerCreate({
|
|
42
|
+
...data,
|
|
43
|
+
...additionalFields,
|
|
44
|
+
})));
|
|
45
|
+
registerValidatedTool(server, 'list_simulations', {
|
|
46
|
+
description: 'Lists simulations. Each simulation pairs one scenario with one personality.',
|
|
47
|
+
inputSchema: ListSimulationResourcesInputSchema,
|
|
48
|
+
}, createToolHandler(async (data) => vapiClient.simulations.simulationControllerFindAll({
|
|
49
|
+
limit: data.limit ?? DEFAULT_LIMIT,
|
|
50
|
+
})));
|
|
51
|
+
registerValidatedTool(server, 'create_simulation', {
|
|
52
|
+
description: 'Creates a simulation that pairs a scenario with a personality. Run it with run_simulations.',
|
|
53
|
+
inputSchema: CreateSimulationInputSchema,
|
|
54
|
+
}, createToolHandler(async (data) => vapiClient.simulations.simulationControllerCreate(data)));
|
|
55
|
+
registerValidatedTool(server, 'list_simulation_suites', {
|
|
56
|
+
description: 'Lists simulation suites: named groups of simulations',
|
|
57
|
+
inputSchema: ListSimulationResourcesInputSchema,
|
|
58
|
+
}, createToolHandler(async (data) => vapiClient.simulationSuites.simulationSuiteControllerFindAll({
|
|
59
|
+
limit: data.limit ?? DEFAULT_LIMIT,
|
|
60
|
+
})));
|
|
61
|
+
registerValidatedTool(server, 'create_simulation_suite', {
|
|
62
|
+
description: 'Creates a simulation suite that groups simulations so they can be run together',
|
|
63
|
+
inputSchema: CreateSimulationSuiteInputSchema,
|
|
64
|
+
}, createToolHandler(async ({ additionalFields, ...data }) => vapiClient.simulationSuites.simulationSuiteControllerCreate({
|
|
65
|
+
...data,
|
|
66
|
+
...additionalFields,
|
|
67
|
+
})));
|
|
68
|
+
registerValidatedTool(server, 'run_simulations', {
|
|
69
|
+
description: 'Starts a simulation run of simulations and/or suites against an assistant or squad. ' +
|
|
70
|
+
'Returns the run with its ID and a dashboard URL to share with the user. Check progress with get_simulation_run.',
|
|
71
|
+
inputSchema: RunSimulationsInputSchema,
|
|
72
|
+
}, createToolHandler(async (data) => {
|
|
73
|
+
const simulations = [
|
|
74
|
+
...(data.simulationIds ?? []).map((simulationId) => ({
|
|
75
|
+
type: 'simulation',
|
|
76
|
+
simulationId,
|
|
77
|
+
})),
|
|
78
|
+
...(data.suiteIds ?? []).map((simulationSuiteId) => ({
|
|
79
|
+
type: 'simulationSuite',
|
|
80
|
+
simulationSuiteId,
|
|
81
|
+
})),
|
|
82
|
+
];
|
|
83
|
+
return vapiClient.simulationRuns.simulationRunControllerCreate({
|
|
84
|
+
simulations,
|
|
85
|
+
target: data.assistantId
|
|
86
|
+
? { type: 'assistant', assistantId: data.assistantId }
|
|
87
|
+
: { type: 'squad', squadId: data.squadId },
|
|
88
|
+
...(data.iterations && { iterations: data.iterations }),
|
|
89
|
+
...(data.transport && { transport: { provider: data.transport } }),
|
|
90
|
+
}, data.idempotencyKey
|
|
91
|
+
? { headers: { 'Idempotency-Key': data.idempotencyKey } }
|
|
92
|
+
: undefined);
|
|
93
|
+
}));
|
|
94
|
+
registerValidatedTool(server, 'list_simulation_runs', {
|
|
95
|
+
description: 'Lists simulation runs, newest first',
|
|
96
|
+
inputSchema: ListSimulationRunsInputSchema,
|
|
97
|
+
}, createToolHandler(async (data) => vapiClient.simulationRuns.simulationRunControllerFindAll({
|
|
98
|
+
limit: data.limit ?? DEFAULT_LIMIT,
|
|
99
|
+
...(data.status && { status: data.status }),
|
|
100
|
+
...(data.targetId && { targetId: data.targetId }),
|
|
101
|
+
})));
|
|
102
|
+
registerValidatedTool(server, 'get_simulation_run', {
|
|
103
|
+
description: 'Gets a simulation run with its status, item counts, and the first 100 item summaries. ' +
|
|
104
|
+
'itemsTruncated indicates omitted items; itemsMetadata preserves API pagination metadata. ' +
|
|
105
|
+
'Use get_simulation_run_item for an item’s transcript and call details.',
|
|
106
|
+
inputSchema: GetSimulationRunInputSchema,
|
|
107
|
+
}, createToolHandler(async (data) => {
|
|
108
|
+
const [run, response] = await Promise.all([
|
|
109
|
+
vapiClient.simulationRuns.simulationRunControllerFindOne({
|
|
110
|
+
id: data.runId,
|
|
111
|
+
}),
|
|
112
|
+
vapiClient.simulationRuns.simulationRunControllerFindItems({
|
|
113
|
+
id: data.runId,
|
|
114
|
+
limit: 100,
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
const page = response;
|
|
118
|
+
const items = Array.isArray(page) ? page : page.results;
|
|
119
|
+
return {
|
|
120
|
+
...run,
|
|
121
|
+
items: items.map(summarizeRunItem),
|
|
122
|
+
itemsTruncated: !Array.isArray(page) && page.metadata.totalItems > items.length,
|
|
123
|
+
...(!Array.isArray(page) && { itemsMetadata: page.metadata }),
|
|
124
|
+
};
|
|
125
|
+
}));
|
|
126
|
+
registerValidatedTool(server, 'get_simulation_run_item', {
|
|
127
|
+
description: 'Gets one simulation run item in full, including results, call snapshots, transcript, and improvement suggestions',
|
|
128
|
+
inputSchema: GetSimulationRunItemInputSchema,
|
|
129
|
+
}, createToolHandler(async (data) => vapiClient.simulationRuns.simulationRunControllerFindItem({
|
|
130
|
+
id: data.runId,
|
|
131
|
+
itemId: data.itemId,
|
|
132
|
+
})));
|
|
133
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/tools/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiBzE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAkBnD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PublicSkills } from '../knowledge/skills.js';
|
|
3
|
+
async function knowledgeResult(action) {
|
|
4
|
+
try {
|
|
5
|
+
return await action();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
return { isError: true, content: [{ type: 'text', text: `Knowledge request failed: ${error instanceof Error ? error.message : 'Unknown upstream error'}` }] };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function jsonResult(data) {
|
|
12
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
13
|
+
}
|
|
14
|
+
export function registerSkillTools(server) {
|
|
15
|
+
const skills = new PublicSkills();
|
|
16
|
+
const annotations = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
|
|
17
|
+
server.registerTool('search_skills', {
|
|
18
|
+
description: 'Find Vapi agent skills in the public VapiAI/skills repository by name, description or content. Omit query to browse. Use load_skill to read a result.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
query: z.string().trim().max(500).default('').describe('Keywords describing the Vapi task, or empty to browse all skills.'),
|
|
21
|
+
limit: z.number().int().min(1).max(30).default(10),
|
|
22
|
+
}, annotations,
|
|
23
|
+
}, ({ query, limit }) => knowledgeResult(async () => jsonResult(await skills.search(query, limit))));
|
|
24
|
+
server.registerTool('load_skill', {
|
|
25
|
+
description: 'Read a public Vapi skill or one of its supporting files. Start with SKILL.md, then load relevant files from the returned file list as needed. Returns instructions for this task; does not install skills or execute scripts.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
name: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100).describe('Exact skill name from search_skills.'),
|
|
28
|
+
path: z.string().min(1).max(300).default('SKILL.md').describe('File path relative to the skill directory, such as SKILL.md or references/hooks.md.'),
|
|
29
|
+
}, annotations,
|
|
30
|
+
}, ({ name, path }) => knowledgeResult(async () => jsonResult(await skills.load(name, path))));
|
|
31
|
+
}
|
package/dist/tools/tool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/tools/tool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/tools/tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAQ,MAAM,qBAAqB,CAAC;AAMvD,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,SAAS,EACjB,YAAY,UAAU,SA2DvB,CAAC"}
|
package/dist/tools/tool.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerValidatedTool } from './validation.js';
|
|
2
|
+
import { EmptyInputSchema, GetToolInputSchema, CreateToolInputSchema, UpdateToolInputSchema } from '../schemas/index.js';
|
|
2
3
|
import { transformToolInput, transformUpdateToolInput, transformToolOutput } from '../transformers/index.js';
|
|
3
4
|
import { createToolHandler } from './utils.js';
|
|
4
5
|
export const registerToolTools = (server, vapiClient) => {
|
|
5
|
-
server
|
|
6
|
+
registerValidatedTool(server, 'list_tools', {
|
|
6
7
|
description: 'Lists all Vapi tools',
|
|
7
|
-
inputSchema:
|
|
8
|
+
inputSchema: EmptyInputSchema,
|
|
8
9
|
}, createToolHandler(async () => {
|
|
9
10
|
const tools = await vapiClient.tools.list({ limit: 10 });
|
|
10
11
|
return tools.map(transformToolOutput);
|
|
11
12
|
}));
|
|
12
|
-
server
|
|
13
|
-
|
|
13
|
+
registerValidatedTool(server, 'get_tool', {
|
|
14
|
+
description: 'Gets details of a specific tool',
|
|
15
|
+
inputSchema: GetToolInputSchema,
|
|
16
|
+
}, createToolHandler(async (data) => {
|
|
17
|
+
const tool = await vapiClient.tools.get({ id: data.toolId });
|
|
14
18
|
return transformToolOutput(tool);
|
|
15
19
|
}));
|
|
16
|
-
server
|
|
20
|
+
registerValidatedTool(server, 'create_tool', {
|
|
21
|
+
description: 'Creates a new Vapi tool',
|
|
22
|
+
inputSchema: CreateToolInputSchema,
|
|
23
|
+
}, createToolHandler(async (data) => {
|
|
17
24
|
const createToolDto = transformToolInput(data);
|
|
18
25
|
const tool = await vapiClient.tools.create(createToolDto);
|
|
19
26
|
return transformToolOutput(tool);
|
|
20
27
|
}));
|
|
21
|
-
server
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
registerValidatedTool(server, 'update_tool', {
|
|
29
|
+
description: 'Updates an existing Vapi tool',
|
|
30
|
+
inputSchema: UpdateToolInputSchema,
|
|
31
|
+
}, createToolHandler(async (data) => {
|
|
32
|
+
const existingTool = await vapiClient.tools.get({ id: data.toolId });
|
|
33
|
+
const updateToolDto = transformUpdateToolInput(data, existingTool);
|
|
34
|
+
const tool = await vapiClient.tools.update({
|
|
35
|
+
id: data.toolId,
|
|
36
|
+
body: updateToolDto,
|
|
37
|
+
});
|
|
24
38
|
return transformToolOutput(tool);
|
|
25
39
|
}));
|
|
26
40
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ToolResponse } from './utils.js';
|
|
4
|
+
type Issue = z.core.$ZodIssue;
|
|
5
|
+
type FieldIssue = {
|
|
6
|
+
path: string;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
allowedValues?: unknown[];
|
|
10
|
+
allowedValuesTruncated?: boolean;
|
|
11
|
+
expected?: string;
|
|
12
|
+
minimum?: number;
|
|
13
|
+
maximum?: number;
|
|
14
|
+
format?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function validationIssues(issues: Issue[], input: unknown, prefix?: PropertyKey[]): FieldIssue[];
|
|
17
|
+
export declare function registerValidatedTool<Schema extends z.ZodType>(server: McpServer, name: string, config: {
|
|
18
|
+
description: string;
|
|
19
|
+
inputSchema: Schema;
|
|
20
|
+
}, handler: (params: z.output<Schema>) => Promise<ToolResponse>): import("@modelcontextprotocol/sdk/server/mcp.js").RegisteredTool;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/tools/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,KAAK,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;AAC9B,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;IACvE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACxE,CAAC;AAcF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAE,WAAW,EAAO,GAAG,UAAU,EAAE,CA8C1G;AAWD,wBAAgB,qBAAqB,CAAC,MAAM,SAAS,CAAC,CAAC,OAAO,EAC5D,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EACpD,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,oEAsB7D"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { summarizeInputSchema } from './input-schema.js';
|
|
3
|
+
const MAX_ISSUES = 12;
|
|
4
|
+
const MAX_OPTIONS = 8;
|
|
5
|
+
const at = (input, path) => path.reduce((value, key) => value !== null && typeof value === 'object' ? value[key] : undefined, input);
|
|
6
|
+
const fieldPath = (path) => path.map((key, index) => typeof key === 'number' ? `[${key}]` : `${index ? '.' : ''}${String(key)}`).join('') || '$';
|
|
7
|
+
const matchesShape = (issues, kind) => !issues.some(issue => issue.path.length === 0 && (issue.code === 'invalid_type' && issue.expected !== kind ||
|
|
8
|
+
issue.code === 'invalid_union' && issue.errors.every(branch => !matchesShape(branch, kind))));
|
|
9
|
+
// Select the supplied provider's branch before formatting a union failure.
|
|
10
|
+
// Otherwise a single forbidden field can emit dozens of unrelated providers.
|
|
11
|
+
export function validationIssues(issues, input, prefix = []) {
|
|
12
|
+
return issues.flatMap((issue) => {
|
|
13
|
+
const path = [...prefix, ...issue.path];
|
|
14
|
+
if (issue.code === 'invalid_union') {
|
|
15
|
+
const value = at(input, path);
|
|
16
|
+
const kind = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value;
|
|
17
|
+
const compatible = issue.errors.filter(branch => matchesShape(branch, kind));
|
|
18
|
+
if (compatible.length === 1)
|
|
19
|
+
return validationIssues(compatible[0], input, path);
|
|
20
|
+
let record = value && typeof value === 'object' ? value : {};
|
|
21
|
+
// LLM configuration also accepts a JSON string. Decode only to select
|
|
22
|
+
// the correct branch; submitted values never enter the error response.
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
try {
|
|
25
|
+
const decoded = JSON.parse(value);
|
|
26
|
+
if (decoded && typeof decoded === 'object')
|
|
27
|
+
record = decoded;
|
|
28
|
+
}
|
|
29
|
+
catch { /* The JSON parser's existing issue supplies the correction. */ }
|
|
30
|
+
}
|
|
31
|
+
const missingDiscriminator = compatible.length ? ['provider', 'type'].find(key => compatible.every(branch => branch.some(child => child.path.length === 1 && child.path[0] === key && child.code === 'invalid_value'))) : undefined;
|
|
32
|
+
const discriminator = missingDiscriminator ?? (typeof record.provider === 'string' ? 'provider' : typeof record.type === 'string' ? 'type' : undefined);
|
|
33
|
+
if (discriminator) {
|
|
34
|
+
const matches = compatible.filter(branch => !branch.some(child => child.path.length === 1 && child.path[0] === discriminator && child.code === 'invalid_value'));
|
|
35
|
+
if (matches.length === 1)
|
|
36
|
+
return validationIssues(matches[0], input, path);
|
|
37
|
+
if (matches.length === 0) {
|
|
38
|
+
const options = compatible.flatMap(branch => branch.flatMap(child => child.code === 'invalid_value' && child.path[0] === discriminator ? child.values : []));
|
|
39
|
+
return [optionIssue([...path, discriminator], options)];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (compatible.length && compatible.every(branch => branch.every(child => child.path.length === 0 && child.code === 'invalid_value'))) {
|
|
43
|
+
return [optionIssue(path, compatible.flatMap(branch => branch.flatMap(child => child.code === 'invalid_value' ? child.values : [])))];
|
|
44
|
+
}
|
|
45
|
+
const expected = [...new Set(issue.errors.flatMap(branch => branch.flatMap(child => child.code === 'invalid_type' && child.path.length === 0 ? [child.expected] : [])))];
|
|
46
|
+
return [{ path: fieldPath(path), code: 'invalid_union', message: expected.length ? `Expected ${expected.join(' or ')}.` : 'Does not match the supported configuration; check the API schema for this field.' }];
|
|
47
|
+
}
|
|
48
|
+
if (issue.code === 'unrecognized_keys')
|
|
49
|
+
return issue.keys.map(key => ({
|
|
50
|
+
path: fieldPath([...path, key]), code: 'unrecognized_key',
|
|
51
|
+
message: 'This field is not supported for the selected provider or configuration; remove it.',
|
|
52
|
+
}));
|
|
53
|
+
if (issue.code === 'invalid_value')
|
|
54
|
+
return [optionIssue(path, issue.values)];
|
|
55
|
+
const detail = { path: fieldPath(path), code: issue.code, message: issue.message.slice(0, 300) };
|
|
56
|
+
if (issue.code === 'invalid_type')
|
|
57
|
+
detail.expected = issue.expected;
|
|
58
|
+
if (issue.code === 'too_big' && typeof issue.maximum === 'number')
|
|
59
|
+
detail.maximum = issue.maximum;
|
|
60
|
+
if (issue.code === 'too_small' && typeof issue.minimum === 'number')
|
|
61
|
+
detail.minimum = issue.minimum;
|
|
62
|
+
if (issue.code === 'invalid_format')
|
|
63
|
+
detail.format = issue.format;
|
|
64
|
+
return [detail];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function optionIssue(path, values) {
|
|
68
|
+
const unique = [...new Set(values)];
|
|
69
|
+
const allowedValues = unique.slice(0, MAX_OPTIONS);
|
|
70
|
+
return {
|
|
71
|
+
path: fieldPath(path), code: 'invalid_value', allowedValues, allowedValuesTruncated: unique.length > MAX_OPTIONS,
|
|
72
|
+
message: `Expected one of: ${allowedValues.map(value => JSON.stringify(value)).join(', ')}${unique.length > MAX_OPTIONS ? ` (and ${unique.length - MAX_OPTIONS} more; see the API schema for this field)` : ''}.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function registerValidatedTool(server, name, config, handler) {
|
|
76
|
+
const schema = config.inputSchema;
|
|
77
|
+
// Advertise a compact input summary, but run the full parser exactly once. This
|
|
78
|
+
// boundary lets us format validation failures before the SDK stringifies a
|
|
79
|
+
// raw ZodError. No request reaches the API handler before parsing succeeds.
|
|
80
|
+
const transportSchema = z.object({}).passthrough().meta(summarizeInputSchema(z.toJSONSchema(schema, { io: 'input', target: 'draft-07' })));
|
|
81
|
+
return server.registerTool(name, { description: config.description, inputSchema: transportSchema }, async (raw) => {
|
|
82
|
+
const parsed = await schema.safeParseAsync(raw);
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
const all = validationIssues(parsed.error.issues, raw);
|
|
85
|
+
const issues = all.slice(0, MAX_ISSUES);
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: 'text', text: `Input validation error for ${name}:\n${issues.map(issue => `- ${issue.path}: ${issue.message}`).join('\n')}${all.length > MAX_ISSUES ? `\n${all.length - MAX_ISSUES} additional issues omitted.` : ''}\nCorrect the listed fields and retry.` }],
|
|
89
|
+
structuredContent: { validationError: { code: 'INVALID_ARGUMENTS', tool: name, issues, truncated: all.length > MAX_ISSUES } },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return handler(parsed.data);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -2,12 +2,12 @@ import { Vapi } from '@vapi-ai/server-sdk';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { CreateAssistantInputSchema, CallInputSchema, AssistantOutputSchema, CallOutputSchema, PhoneNumberOutputSchema, ToolOutputSchema, UpdateAssistantInputSchema, CreateToolInputSchema, UpdateToolInputSchema } from '../schemas/index.js';
|
|
4
4
|
export declare function transformAssistantInput(input: z.infer<typeof CreateAssistantInputSchema>): Vapi.CreateAssistantDto;
|
|
5
|
-
export declare function transformUpdateAssistantInput(input: z.infer<typeof UpdateAssistantInputSchema
|
|
5
|
+
export declare function transformUpdateAssistantInput(input: z.infer<typeof UpdateAssistantInputSchema>, existing?: Vapi.Assistant): Vapi.UpdateAssistantDto;
|
|
6
6
|
export declare function transformAssistantOutput(assistant: Vapi.Assistant): z.infer<typeof AssistantOutputSchema>;
|
|
7
7
|
export declare function transformCallInput(input: z.infer<typeof CallInputSchema>): Vapi.CreateCallDto;
|
|
8
8
|
export declare function transformCallOutput(call: Vapi.Call): z.infer<typeof CallOutputSchema>;
|
|
9
9
|
export declare function transformPhoneNumberOutput(phoneNumber: any): z.infer<typeof PhoneNumberOutputSchema>;
|
|
10
10
|
export declare function transformToolInput(input: z.infer<typeof CreateToolInputSchema>): any;
|
|
11
|
-
export declare function transformUpdateToolInput(input: z.infer<typeof UpdateToolInputSchema
|
|
12
|
-
export declare function transformToolOutput(tool: Vapi.
|
|
11
|
+
export declare function transformUpdateToolInput(input: z.infer<typeof UpdateToolInputSchema>, existing?: Vapi.GetToolsResponse): any;
|
|
12
|
+
export declare function transformToolOutput(tool: Vapi.GetToolsResponse): z.infer<typeof ToolOutputSchema>;
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transformers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,qBAAqB,CAAC;AAI7B,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,GAChD,IAAI,CAAC,kBAAkB,CA2CzB;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transformers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,qBAAqB,CAAC;AAI7B,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,GAChD,IAAI,CAAC,kBAAkB,CA2CzB;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,EACjD,QAAQ,CAAC,EAAE,IAAI,CAAC,SAAS,GACxB,IAAI,CAAC,kBAAkB,CAyEzB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,IAAI,CAAC,SAAS,GACxB,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAuBvC;AA2BD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,GACrC,IAAI,CAAC,aAAa,CAwBpB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,CAAC,IAAI,GACd,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAgBlC;AAID,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,GAAG,GACf,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CASzC;AAID,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,GAC3C,GAAG,CAkEL;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,EAC5C,QAAQ,CAAC,EAAE,IAAI,CAAC,gBAAgB,GAC/B,GAAG,CA+DL;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,CAAC,gBAAgB,GAC1B,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAiBlC"}
|
|
@@ -35,42 +35,44 @@ export function transformAssistantInput(input) {
|
|
|
35
35
|
}
|
|
36
36
|
return assistantDto;
|
|
37
37
|
}
|
|
38
|
-
export function transformUpdateAssistantInput(input) {
|
|
38
|
+
export function transformUpdateAssistantInput(input, existing) {
|
|
39
39
|
const updateDto = {};
|
|
40
40
|
if (input.name) {
|
|
41
41
|
updateDto.name = input.name;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
const hasToolIds = !!input.toolIds && input.toolIds.length > 0;
|
|
44
|
+
if (input.llm || hasToolIds || input.instructions) {
|
|
45
|
+
// PATCH replaces `model` as a whole, so start from the assistant's current
|
|
46
|
+
// model. Sending only the changed fields drops `provider`/`model` and the
|
|
47
|
+
// API rejects the update.
|
|
48
|
+
const existingModel = existing?.model ?? {};
|
|
49
|
+
const providerChanged = !!input.llm && input.llm.provider !== existingModel.provider;
|
|
50
|
+
// Provider-specific settings may not be valid for a new provider, so only
|
|
51
|
+
// the prompt and tools carry over when the provider changes.
|
|
52
|
+
const model = providerChanged
|
|
53
|
+
? {
|
|
54
|
+
...(existingModel.messages && { messages: existingModel.messages }),
|
|
55
|
+
...(existingModel.toolIds && { toolIds: existingModel.toolIds }),
|
|
56
|
+
}
|
|
57
|
+
: { ...existingModel };
|
|
58
|
+
if (input.llm) {
|
|
59
|
+
model.provider = input.llm.provider;
|
|
60
|
+
model.model = input.llm.model;
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (input.toolIds && input.toolIds.length > 0) {
|
|
62
|
-
updateDto.model = { toolIds: input.toolIds };
|
|
62
|
+
if (hasToolIds) {
|
|
63
|
+
model.toolIds = input.toolIds;
|
|
63
64
|
}
|
|
64
65
|
if (input.instructions) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
updateDto.model.messages = [
|
|
66
|
+
const otherMessages = (model.messages ?? []).filter((message) => message.role !== 'system');
|
|
67
|
+
model.messages = [
|
|
68
68
|
{
|
|
69
69
|
role: 'system',
|
|
70
70
|
content: input.instructions,
|
|
71
71
|
},
|
|
72
|
+
...otherMessages,
|
|
72
73
|
];
|
|
73
74
|
}
|
|
75
|
+
updateDto.model = model;
|
|
74
76
|
}
|
|
75
77
|
if (input.transcriber) {
|
|
76
78
|
updateDto.transcriber = {
|
|
@@ -94,6 +96,8 @@ export function transformUpdateAssistantInput(input) {
|
|
|
94
96
|
return updateDto;
|
|
95
97
|
}
|
|
96
98
|
export function transformAssistantOutput(assistant) {
|
|
99
|
+
const voiceModel = getAssistantVoiceModel(assistant.voice);
|
|
100
|
+
const transcriberModel = getAssistantTranscriberModel(assistant.transcriber);
|
|
97
101
|
return {
|
|
98
102
|
id: assistant.id,
|
|
99
103
|
createdAt: assistant.createdAt,
|
|
@@ -106,11 +110,11 @@ export function transformAssistantOutput(assistant) {
|
|
|
106
110
|
voice: {
|
|
107
111
|
provider: assistant.voice?.provider || '11labs',
|
|
108
112
|
voiceId: getAssistantVoiceId(assistant.voice),
|
|
109
|
-
model:
|
|
113
|
+
...(voiceModel ? { model: voiceModel } : {}),
|
|
110
114
|
},
|
|
111
115
|
transcriber: {
|
|
112
116
|
provider: assistant.transcriber?.provider || 'deepgram',
|
|
113
|
-
model:
|
|
117
|
+
...(transcriberModel ? { model: transcriberModel } : {}),
|
|
114
118
|
},
|
|
115
119
|
toolIds: assistant.model?.toolIds || [],
|
|
116
120
|
};
|
|
@@ -245,13 +249,22 @@ export function transformToolInput(input) {
|
|
|
245
249
|
default:
|
|
246
250
|
throw new Error(`Unsupported tool type: ${input.type}`);
|
|
247
251
|
}
|
|
252
|
+
if (input.additionalFields) {
|
|
253
|
+
Object.assign(toolDto, input.additionalFields);
|
|
254
|
+
}
|
|
248
255
|
return toolDto;
|
|
249
256
|
}
|
|
250
|
-
export function transformUpdateToolInput(input) {
|
|
257
|
+
export function transformUpdateToolInput(input, existing) {
|
|
251
258
|
let updateDto = {};
|
|
259
|
+
// PATCH replaces `function` and `server` as a whole, so start from the
|
|
260
|
+
// tool's current values. Sending only the changed fields drops
|
|
261
|
+
// `function.name` and the API rejects the update.
|
|
262
|
+
const existingFunction = existing && 'function' in existing ? existing.function : undefined;
|
|
263
|
+
const existingServer = existing && 'server' in existing ? existing.server : undefined;
|
|
252
264
|
// Add function definition if name and description are provided
|
|
253
265
|
if (input.name || input.description) {
|
|
254
266
|
updateDto.function = {
|
|
267
|
+
...existingFunction,
|
|
255
268
|
...(input.name && { name: input.name }),
|
|
256
269
|
...(input.description && { description: input.description }),
|
|
257
270
|
};
|
|
@@ -272,10 +285,12 @@ export function transformUpdateToolInput(input) {
|
|
|
272
285
|
}
|
|
273
286
|
else {
|
|
274
287
|
updateDto.function = {
|
|
288
|
+
...existingFunction,
|
|
275
289
|
parameters: input.function.parameters,
|
|
276
290
|
};
|
|
277
291
|
}
|
|
278
292
|
updateDto.server = {
|
|
293
|
+
...existingServer,
|
|
279
294
|
url: input.function.server.url,
|
|
280
295
|
...(input.function.server.headers && { headers: input.function.server.headers }),
|
|
281
296
|
};
|
|
@@ -295,20 +310,26 @@ export function transformUpdateToolInput(input) {
|
|
|
295
310
|
if (input.apiRequest.timeoutSeconds)
|
|
296
311
|
updateDto.timeoutSeconds = input.apiRequest.timeoutSeconds;
|
|
297
312
|
}
|
|
313
|
+
if (input.additionalFields) {
|
|
314
|
+
Object.assign(updateDto, input.additionalFields);
|
|
315
|
+
}
|
|
298
316
|
return updateDto;
|
|
299
317
|
}
|
|
300
318
|
export function transformToolOutput(tool) {
|
|
319
|
+
// Not every tool type in SDK 3.x has `function` or `server`.
|
|
320
|
+
const toolFunction = 'function' in tool ? tool.function : undefined;
|
|
321
|
+
const toolServer = 'server' in tool ? tool.server : undefined;
|
|
301
322
|
return {
|
|
302
323
|
id: tool.id,
|
|
303
324
|
createdAt: tool.createdAt,
|
|
304
325
|
updatedAt: tool.updatedAt,
|
|
305
326
|
type: tool.type || '',
|
|
306
|
-
name:
|
|
307
|
-
description:
|
|
308
|
-
parameters:
|
|
327
|
+
name: toolFunction?.name || '',
|
|
328
|
+
description: toolFunction?.description || '',
|
|
329
|
+
parameters: toolFunction?.parameters || {},
|
|
309
330
|
server: {
|
|
310
|
-
url:
|
|
311
|
-
headers:
|
|
331
|
+
url: toolServer?.url || '',
|
|
332
|
+
headers: toolServer?.headers || {},
|
|
312
333
|
}
|
|
313
334
|
};
|
|
314
335
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vapi-ai/mcp-server",
|
|
3
3
|
"description": "Build AI voice assistants and phone agents with Vapi through the Model Context Protocol",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"tool-calling"
|
|
33
33
|
],
|
|
34
34
|
"bin": {
|
|
35
|
-
"
|
|
35
|
+
"mcp-server": "dist/index.js"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"prepare": "npm run build",
|
|
39
39
|
"clean": "shx rm -rf dist",
|
|
40
|
-
"build": "npm run clean && tsc -p tsconfig.build.json && shx chmod +x dist/*.js",
|
|
40
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && shx cp src/schemas/assistant-openapi.json src/schemas/simulation-openapi.json dist/schemas/ && shx chmod +x dist/*.js",
|
|
41
41
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
42
42
|
"start": "node dist/index.js",
|
|
43
43
|
"dev": "tsx watch src/index.ts",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"test": "VAPI_TOKEN=test-mock-token NODE_OPTIONS=--experimental-vm-modules jest --runInBand --testPathIgnorePatterns='.*-live\\.test\\.ts$'",
|
|
50
50
|
"test:unit": "VAPI_TOKEN=test-mock-token NODE_OPTIONS=--experimental-vm-modules jest --runInBand src/__tests__/mcp-tool-execution.test.ts src/__tests__/mcp-tools-list.test.ts",
|
|
51
51
|
"test:docs": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand src/__tests__/documentation-tools.test.ts",
|
|
52
|
+
"schemas:snapshot": "node scripts/snapshot-assistant-validation.mjs",
|
|
52
53
|
"pretest:stdio": "npm run build",
|
|
53
54
|
"test:stdio": "VAPI_TOKEN=test-mock-token NODE_OPTIONS=--experimental-vm-modules jest --runInBand src/__tests__/mcp-stdio-compat.test.ts",
|
|
54
55
|
"pretest:package": "npm run build",
|
|
@@ -62,16 +63,18 @@
|
|
|
62
63
|
"dist/**/*.d.ts.map"
|
|
63
64
|
],
|
|
64
65
|
"dependencies": {
|
|
65
|
-
"@modelcontextprotocol/sdk": "1.
|
|
66
|
-
"@vapi-ai/server-sdk": "^0.
|
|
66
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
67
|
+
"@vapi-ai/server-sdk": "^3.0.0",
|
|
67
68
|
"dotenv": "^16.4.7",
|
|
68
|
-
"
|
|
69
|
-
"zod
|
|
69
|
+
"yaml": "^2.9.1",
|
|
70
|
+
"zod": "4.6.7"
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
|
72
73
|
"@modelcontextprotocol/inspector": "1.0.1",
|
|
73
74
|
"@types/jest": "^29.5.14",
|
|
74
75
|
"@types/node": "^22.14.0",
|
|
76
|
+
"ajv": "8.20.0",
|
|
77
|
+
"ajv-formats": "3.0.1",
|
|
75
78
|
"jest": "^29.7.0",
|
|
76
79
|
"shx": "^0.4.0",
|
|
77
80
|
"ts-jest": "^29.3.1",
|