JuliusBrussee/caveman · error
tools must be unique caveman_* names
Error message
tools must be unique caveman_* names
What it means
Thrown inside loadSurfaces() in skills/verbs-gate.mjs when the engine-mcp-tools.json tools array is non-empty-safe: it rejects an empty array, duplicate names, or any entry that is not a string matching /^caveman_[a-z_]+$/. These rules keep the MCP tool namespace uniform (lowercase snake-case prefixed with caveman_) so verb collision checks downstream are reliable. The throw is caught and reported as `engine-mcp-tools.json: tools must be unique caveman_* names` in the gate error list.
Source
Thrown at skills/verbs-gate.mjs:81
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();
if (JSON.stringify(declared) !== JSON.stringify(source)) {
errors.push("engine-mcp-tools.json drifted from public/mcp/engine_tools.go");
}
}
const agentMcpSrc = readOptional(join(cliDir, "src", "agent-mcp.ts"));
for (const m of agentMcpSrc.matchAll(/name:\s*"(caveman_[a-z_]+)"/g)) mcpTools.add(m[1]);
if (mcpTools.size === 0) errors.push("could not parse MCP tool names");
View on GitHub (pinned to 27d5a3981a)
Solutions
- Make every entry a unique string matching caveman_[a-z_]+ (e.g. "caveman_tool_search") and ensure at least one entry exists.
- Remove duplicates introduced by merges; sort the array to make future duplicates obvious.
- Cross-check against public/mcp/engine_tools.go — the gate compares the manifest against `Tool\w+ = "caveman_..."` constants and reports drift.
Example fix
// before
{ "schema_version": "1", "tools": ["caveman_compress", "caveman_compress", "Compress"] }
// after
{ "schema_version": "1", "tools": ["caveman_compress", "caveman_expand"] } Defensive patterns
Strategy: type-guard
Type guard
function hasValidToolNames(doc) {
return Array.isArray(doc.tools) && doc.tools.length > 0
&& new Set(doc.tools).size === doc.tools.length
&& doc.tools.every((n) => typeof n === "string" && /^caveman_[a-z_]+$/.test(n));
} Prevention
- Name MCP tools caveman_ + lowercase snake_case from the start.
- After merges, sort and de-duplicate the tools array before committing; the gate also diffs it against public/mcp/engine_tools.go.
When it happens
Trigger: A manifest with zero tools; the same name listed twice after a merge; a name like "Compress", "caveman-compress", "caveman_WebSearch", or a non-string entry (number/object).
Common situations: Merge conflicts duplicating entries; hand-adding a tool with camelCase or a hyphen; generator emitting the raw tool name without the caveman_ prefix.
Related errors
- schema_version must be "1" and tools must be an array
- verbs is not an array
- cave_duplicate_tool_name
- cave_reserved_tool_name:${declared.name}
- caveman agent: invalid agent id ${JSON.stringify(options.id)
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/194ac2816b0bf206.
Report an issue: GitHub.