Skip to content

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 →