Today nao init creates agent/tools/ (cli/nao_core/commands/init.py:88) but no code reads from it. Custom executable tools are only possible by authoring a full MCP server in agent/mcps/mcp.json — heavy for a single one-shot tool.
Rationale:
- Single-file tool is much lighter than an MCP server (no protocol, no long-running process).
- Mirrors the ergonomics of
agent/skills/: drop a file, hot reload.
- Closes the extension surface: skills (declarative guidance) + MCPs (external server) + tools (single executable).
Proposed implementation:
Each agent/tools/<name>.json declares one tool:
{
"name": "get_order",
"description": "Get order details by order ID",
"inputSchema": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
},
"command": "python",
"args": ["scripts/get_order.py"],
"input": "stdin",
"env": {}
}
Backend gains a customToolService mirroring skillService (file watcher, lazy load). For each spec, it builds a Tool via createTool({ description, inputSchema, execute }) where execute spawns the command, writes input as JSON to stdin, reads JSON from stdout. The resulting Record<string, Tool> merges into getTools() in apps/backend/src/agents/tools/index.ts alongside mcpTools — no LLM-side change.
CLI side: new cli/nao_core/config/tools/ module mirroring skills/, with an optional get-order example.
Today
nao initcreatesagent/tools/(cli/nao_core/commands/init.py:88) but no code reads from it. Custom executable tools are only possible by authoring a full MCP server inagent/mcps/mcp.json— heavy for a single one-shot tool.Rationale:
agent/skills/: drop a file, hot reload.Proposed implementation:
Each
agent/tools/<name>.jsondeclares one tool:{ "name": "get_order", "description": "Get order details by order ID", "inputSchema": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] }, "command": "python", "args": ["scripts/get_order.py"], "input": "stdin", "env": {} }Backend gains a
customToolServicemirroringskillService(file watcher, lazy load). For each spec, it builds aToolviacreateTool({ description, inputSchema, execute })whereexecutespawns the command, writes input as JSON to stdin, reads JSON from stdout. The resultingRecord<string, Tool>merges intogetTools()inapps/backend/src/agents/tools/index.tsalongsidemcpTools— no LLM-side change.CLI side: new
cli/nao_core/config/tools/module mirroringskills/, with an optionalget-orderexample.