JuliusBrussee/caveman · error
schema_version must be "1" and tools must be an array
Error message
schema_version must be "1" and tools must be an array
What it means
Thrown inside loadSurfaces() in skills/verbs-gate.mjs while reading skills/engine-mcp-tools.json, the manifest of MCP tool names exposed by the Go engine. The gate pins the manifest format: schema_version must be exactly the string "1" and tools must be an array. Like the reserved-verbs error, it is caught and reported as `engine-mcp-tools.json: <message>` in the gate's error list.
Source
Thrown at skills/verbs-gate.mjs:73
topLevel = new Set(doc.verbs);
} catch (error) {
errors.push(`reserved-verbs.json: ${error.message}`);
}
// `caveman tools <verb>` / `caveman cloud <verb>` namespaces.
const indexSrc = readOptional(join(cliDir, "src", "index.ts"));
const toolVerbs = discoveryVerbs(indexSrc, "const TOOL_DISCOVERY");
const cloudVerbs = discoveryVerbs(indexSrc, "const CLOUD_DISCOVERY");
if (toolVerbs.size === 0) errors.push("could not parse TOOL_DISCOVERY verbs from CLI index.ts");
if (cloudVerbs.size === 0) errors.push("could not parse CLOUD_DISCOVERY verbs from CLI index.ts");
// MCP tool names: engine server (Go) + agent-native server (TS).
const mcpTools = new Set();
let declaredEngineTools = [];
try {
const manifest = JSON.parse(readFileSync(join(skillsDir, "engine-mcp-tools.json"), "utf8"));
if (manifest.schema_version !== "1" || !Array.isArray(manifest.tools)) {
throw new Error('schema_version must be "1" and tools must be an array');
}
declaredEngineTools = [...manifest.tools];
if (
declaredEngineTools.length === 0
|| new Set(declaredEngineTools).size !== declaredEngineTools.length
|| declaredEngineTools.some((name) => typeof name !== "string" || !/^caveman_[a-z_]+$/.test(name))
) {
throw new Error("tools must be unique caveman_* names");
}
for (const name of declaredEngineTools) mcpTools.add(name);
} catch (error) {
errors.push(`engine-mcp-tools.json: ${error.message}`);
}
const engineSrc = readOptional(join(skillsDir, "..", "mcp", "engine_tools.go"));
const sourceEngineTools = [...engineSrc.matchAll(/Tool\w+\s*=\s*"(caveman_[a-z_]+)"/g)].map((match) => match[1]);
if (sourceEngineTools.length > 0) {
const declared = [...declaredEngineTools].sort();
const source = [...new Set(sourceEngineTools)].sort();View on GitHub (pinned to 27d5a3981a)
Solutions
- Set `"schema_version": "1"` (string) and make `tools` an array of name strings in engine-mcp-tools.json.
- If the schema genuinely evolved, update skills/verbs-gate.mjs to accept the new version — do not silence the error.
- Re-run the verbs gate; also expect the engine_tools.go drift check to validate the names.
Example fix
// before: engine-mcp-tools.json
{ "schema_version": 1, "tools": { "a": "caveman_a" } }
// after
{ "schema_version": "1", "tools": ["caveman_compress", "caveman_expand"] } Defensive patterns
Strategy: type-guard
Type guard
function isEngineToolsManifest(doc) {
return doc !== null && typeof doc === "object"
&& doc.schema_version === "1" && Array.isArray(doc.tools);
} Prevention
- Keep schema_version as the string "1" — a numeric 1 fails the strict check.
- Generate the manifest with a script rather than by hand so the shape stays stable.
When it happens
Trigger: engine-mcp-tools.json has schema_version 2, a numeric 1 instead of "1", or missing/renamed schema_version; or `tools` is an object/absent instead of an array.
Common situations: Bumping the manifest version without updating the gate; regenerating the manifest with a script that emits different casing or shape; hand-merging a conflict that drops a field.
Related errors
- tools must be unique caveman_* names
- verbs is not an array
- ${key} is required
- filters must be an object
- unknown trace filter(s): ${unknown.sort().join(", ")}
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/67a3f6d2894e443a.
Report an issue: GitHub.