How to design and register a new MCP tool — input schema (Zod), output contract, descriptions, tests. Read before adding any new tool to Tools/src/tools/.
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
How to design and register a new MCP tool — input schema (Zod), output contract, descriptions, tests. Read before adding any new tool to Tools/src/tools/.
MCP tool schema
Checklist for adding a new tool. Read end-to-end before writing the tool file.
Checklist
Tool belongs in a group file under Tools/src/tools/<group>.ts. Pick existing if domain matches; create new if it doesn't.
Tool name follows <domain>_<verb>[<_qualifier>]: object_create, material_set_principled_param, bone_add, socket_add, export_fbx.
Zod schema for every input parameter, with .describe() on each.
Return type is Promise<ToolResult<T>> where T is the success payload shape.
On success: populate data, refs, optionally nextSteps, optionally warnings.
On failure: ok: false, errorCode from the registry, warnings with detail.
Description string written for the LLM consuming the tool — under 200 chars for simple, structured multi-line for complex (especially exports).
Integration test in Tools/test/tools/<tool-name>.test.ts covering happy path, every errorCode branch, idempotency where applicable.
Python handler exists if needed, registered via @handler("METHOD", "/path"), follows the handler pattern from .claude/rules/python-blender.md.
// Tools/src/tools/my-domain.tsimport { z } from"zod";
importtype { McpServer } from"@modelcontextprotocol/sdk/server/mcp.js";
import { blenderPost } from"../blender-bridge.js";
importtype { ToolResult } from"../types.js";
exportfunctionregisterMyDomainTools(server: McpServer): void {
server.tool(
"my_domain_do_thing",
"Short imperative description aimed at the LLM. Under 200 chars. What it does, what it returns, when to call it next.",
{
objectName: z.string().describe("Object name (data-block key in bpy.data.objects)"),
// ...
},
async (input): Promise<ToolResult<{ /* payload */ }>> => {
try {
const res = await blenderPost<{ /* python response */ }>("/my_domain/do_thing", input);
return {
ok: true,
data: { /* mapped */ },
refs: { /* IDs */ },
nextSteps: ["call ... next"],
};
} catch (err) {
// narrow to known errorCodes when possiblereturn { ok: false, errorCode: "BLENDER_HTTP_FAILED", warnings: [String(err)] };
}
}
);
}
Then add registerMyDomainTools(server) to Tools/src/index.ts.
Python handler template
# BlenderAgent/handlers/my_domain.pyimport bpy
from ..server import handler, run_on_main, HandlerError, with_mode
@handler("POST", "/my_domain/do_thing")defdo_thing(req):
object_name = req["object_name"] # snake_case on the wire# ... pull other paramsdefmain():
obj = bpy.data.objects.get(object_name)
if obj isNone:
raise HandlerError("OBJECT_NOT_FOUND", f"object {object_name!r} not found")
# ... mutation
bpy.ops.ed.undo_push(message="Do thing on object")
return {"object_name": obj.name}
return run_on_main(main)
Then ensure the module is imported in BlenderAgent/handlers/__init__.py so the @handler decorator runs.
Description writing — for the LLM
The description is the agent's only hint at when to call the tool. Write it for that.
✅ "Add a bone to an armature. Switches to Edit Mode automatically and restores afterwards. Returns the bone name. Call bone_set_transform or socket_add next."
❌ "Adds a bone" (too terse — agent guesses inputs).
❌ "Switches active object to the armature, enters Edit Mode via bpy.ops.object.mode_set, calls armature.data.edit_bones.new with name parameter, sets head/tail from request payload..." (noise; wastes tokens).
For complex tools (export_* especially): multi-section structured description quoting the operator name and Blender's tooltip per parameter group.
Tests are mandatory
Every new tool → Tools/test/tools/<tool-name>.test.ts.