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

  1. Set `"schema_version": "1"` (string) and make `tools` an array of name strings in engine-mcp-tools.json.
  2. If the schema genuinely evolved, update skills/verbs-gate.mjs to accept the new version — do not silence the error.
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/67a3f6d2894e443a. Report an issue: GitHub.