1
16
17const ALLOWED_TYPES = new Set(["string", "number", "boolean", "integer", "array", "object"]);
18const ALLOWED_KEYWORDS = new Set([
19 "type",
20 "properties",
21 "required",
22 "items",
23 "enum",
24 "description",
25]);
26
27export interface ToolValidationResult {
28 ok: boolean;
29
30 error?: string;
31}
32
33
34export function validateToolParameters(parameters: unknown, path = "$"): ToolValidationResult {
35 if (typeof parameters !== "object" || parameters === null || Array.isArray(parameters)) {
36 return { ok: false, error: `${path}: parameters must be an object` };
37 }
38 const schema = parameters as Record<string, unknown>;
39
40 for (const key of Object.keys(schema)) {
41 if (!ALLOWED_KEYWORDS.has(key)) {
42 return { ok: false, error: `${path}: keyword "${key}" is not in the allowed tool-schema subset` };
43 }
44 }
45
46 if (schema.type !== undefined) {
47 const type = schema.type;
48 if (typeof type !== "string" || !ALLOWED_TYPES.has(type)) {
49 return { ok: false, error: `${path}: type must be one of ${[...ALLOWED_TYPES].join("|")}` };
50 }
51 if (type === "array") {
52 if (schema.items === undefined) {
53 return { ok: false, error: `${path}: array type requires "items"` };
54 }
55 const items = validateToolParameters(schema.items, `${path}.items`);
56 if (!items.ok) return items;
57 }
58 if (type === "object") {
59 if (schema.properties !== undefined) {
60 if (typeof schema.properties !== "object" || schema.properties === null || Array.isArray(schema.properties)) {
61 return { ok: false, error: `${path}.properties must be an object` };
62 }
63 const props = schema.properties as Record<string, unknown>;
64 for (const name of Object.keys(props)) {
65 const sub = validateToolParameters(props[name], `${path}.properties.${name}`);
66 if (!sub.ok) return sub;
67 }
68 if (schema.required !== undefined) {
69 if (!Array.isArray(schema.required) || schema.required.some((r) => typeof r !== "string")) {
70 return { ok: false, error: `${path}.required must be an array of strings` };
71 }
72 for (const req of schema.required as string[]) {
73 if (!(req in props)) {
74 return { ok: false, error: `${path}.required lists "${req}" which is not in properties` };
75 }
76 }
77 }
78 }
79 }
80 if (schema.enum !== undefined) {
81 if (!Array.isArray(schema.enum) || schema.enum.length === 0) {
82 return { ok: false, error: `${path}.enum must be a non-empty array` };
83 }
84 }
85 }
86 return { ok: true };
87}
88
89
90export function validateTools(tools: readonly { name: string; description: string; parameters: unknown }[]): ToolValidationResult {
91 const seen = new Set<string>();
92 for (const tool of tools) {
93 if (typeof tool.name !== "string" || !/^[A-Za-z0-9_-]+$/.test(tool.name)) {
94 return { ok: false, error: `tool name "${String(tool.name)}" must match [A-Za-z0-9_-]+` };
95 }
96 if (seen.has(tool.name)) {
97 return { ok: false, error: `duplicate tool name "${tool.name}"` };
98 }
99 seen.add(tool.name);
100 if (typeof tool.description !== "string" || tool.description.length === 0) {
101 return { ok: false, error: `tool "${tool.name}" requires a non-empty description` };
102 }
103 const params = validateToolParameters(tool.parameters, `tool.${tool.name}.parameters`);
104 if (!params.ok) return params;
105 }
106 return { ok: true };
107}
108