Tool Schema Builder — Zig source
Build function-calling and MCP tool schemas that pass strict mode on the first try, and lint pasted ones against the strict-mode contract — additionalProperties, required-sync, defaults, enums — with one-click autofix for every mechanical violation. Runs entirely in your browser.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Tool Schema Builder — strict-mode validation of function-calling / MCP
// tool definitions (OpenAI strict mode / MCP inputSchema contract).
// CosmoDev polyglot showcase port from src/lib/tool-schema.ts. Zig 0.14; an arena
// backs the walk — the parsed tree and the composed paths free together.
const std = @import("std");
const Issue = struct { rule: []const u8, path: []const u8 };
fn isObj(v: std.json.Value) ?std.json.ObjectMap {
return switch (v) { .object => |o| o, else => null };
}
fn str(m: std.json.ObjectMap, k: []const u8) ?[]const u8 {
return switch (m.get(k) orelse return null) { .string => |s| s, else => null };
}
const supported = [_][]const u8{ "string", "number", "integer", "boolean", "object", "array" };
fn listed(req: ?std.json.Value, key: []const u8) bool {
const rv = req orelse return false;
switch (rv) {
.array => |arr| for (arr.items) |r| {
if (r == .string and std.mem.eql(u8, r.string, key)) return true;
},
else => {},
}
return false;
}
// The recursive strict-mode walk — every object nests the same rules.
fn checkObject(a: std.mem.Allocator, path: []const u8, obj: std.json.ObjectMap, issues: *std.ArrayList(Issue)) !void {
const strict = switch (obj.get("additionalProperties") orelse .null) { .bool => |b| !b, else => false };
if (!strict) try issues.append(.{ .rule = "no-additional-properties", .path = path });
const props = isObj(obj.get("properties") orelse .null) orelse return;
var missing: usize = 0;
for (props.keys()) |k| if (!listed(obj.get("required"), k)) { missing += 1; };
if (missing > 0) try issues.append(.{ .rule = "all-required", .path = path });
var it = props.iterator();
while (it.next()) |e| {
const prop = isObj(e.value_ptr.*) orelse continue;
const p = try std.fmt.allocPrint(a, "{s}.properties.{s}", .{ path, e.key_ptr.* });
if (prop.contains("default")) try issues.append(.{ .rule = "no-defaults", .path = p });
const d = str(prop, "description") orelse "";
if (std.mem.trim(u8, d, " \t").len == 0) try issues.append(.{ .rule = "description-present", .path = p });
const t = str(prop, "type") orelse "";
var type_ok = false;
for (supported) |s| if (std.mem.eql(u8, s, t)) { type_ok = true; };
if (!type_ok) try issues.append(.{ .rule = "typed-properties", .path = p });
if (prop.get("enum")) |ev| switch (ev) {
.array => |arr| {
var mixed = false;
var primitive = true;
if (arr.items.len > 0) {
const ft = std.meta.activeTag(arr.items[0]);
primitive = ft == .string or ft == .number or ft == .bool;
for (arr.items) |v| if (std.meta.activeTag(v) != ft) { mixed = true; };
}
if (arr.items.len == 0 or mixed or !primitive) try issues.append(.{ .rule = "enum-values", .path = p });
},
else => {},
};
if (std.mem.eql(u8, t, "array") and isObj(prop.get("items") orelse .null) == null)
try issues.append(.{ .rule = "array-items", .path = p });
if (std.mem.eql(u8, t, "object") and isObj(prop.get("properties") orelse .null) != null)
try checkObject(a, p, prop, issues);
}
}
// Demo: validate a small broken definition and print the issues.
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const a = arena.allocator();
const broken =
\\{"name": "Get_Weather", "input_schema": {"type": "object", "properties": {"city": {"type": "string", "default": "Paris"},
\\"unit": {"type": "string", "description": "celsius or fahrenheit", "enum": ["c", 2]}, "tags": {"type": "array"}}, "required": ["city"]}}
;
const root = try std.json.parseFromSliceLeaky(std.json.Value, a, broken, .{});
var issues = std.ArrayList(Issue).init(a);
if (isObj(root)) |o| {
const nm = str(o, "name") orelse "";
var name_ok = nm.len >= 1 and nm.len <= 64;
for (nm) |c| if (!((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_' or c == '-')) name_ok = false;
if (!name_ok) try issues.append(.{ .rule = "non-empty-name", .path = "name: must be 1-64 chars of [a-z0-9_-]" });
const d = str(o, "description") orelse "";
if (std.mem.trim(u8, d, " \t").len == 0)
try issues.append(.{ .rule = "description-present", .path = "description: the tool needs a description" });
var schema_ok = false;
if (isObj(o.get("input_schema") orelse .null)) |s| if (str(s, "type")) |ty| {
schema_ok = std.mem.eql(u8, ty, "object");
if (schema_ok) try checkObject(a, "input_schema", s, &issues);
};
if (!schema_ok)
try issues.append(.{ .rule = "json-parseable", .path = "input_schema: must be an object with type: \"object\"" });
} else {
try issues.append(.{ .rule = "json-parseable", .path = "$: input must be a JSON object" });
}
const w = std.io.getStdOut().writer();
for (issues.items) |i| try w.print("{s} {s}\n", .{ i.rule, i.path });
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →