One demo, five sources
To an LLM, a tool is a tool is a tool: a named, described, schema’d callable. This one run proves it — a plain function, an MCP server, an agent skill, and a remote A2A agent all sit in the same registry, and then the agent stops to ask you a question before it finishes.
The six beats
Section titled “The six beats”- A plain function — register
add(a, b)withdefineTool. Your code is a tool. - An MCP server — loaded from
mcp.json; its tools appear withsource: "mcp". - An agent skill — a
SKILL.mdunderskills/, loaded on demand via theskilltool. - A remote A2A agent — another agent, served over A2A, called like a function.
- A question — the agent hits a fork and suspends, asking you to decide.
- Resume — your answer flows back in; the loop finishes.
Assembling every source
Section titled “Assembling every source”import { createToolkit, defineTool, createClient, agent } from "toolnexus"
const tk = await createToolkit({ mcpConfig: "./mcp.json", // 2 — MCP server tools skillsDir: "./skills", // 3 — the `skill` tool agents: [agent({ card: reviewerUrl + "/.well-known/agent-card.json" })], // 4 — A2A agent-as-tool // built-ins on by default → 5 — the `question` tool is present})
tk.register(defineTool({ // 1 — a plain function tool name: "add", description: "Add two numbers.", inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] }, run: ({ a, b }) => `${a + b}`,}))The human in the loop
Section titled “The human in the loop”Give the client a waitFor host. When any tool suspends — here, the built-in question tool —
the loop calls waitFor, feeds your answer back in, and resumes:
const client = createClient({ baseUrl, style: "openai", model, waitFor: async (req) => { // req.kind === "question", req.prompt renders the choices return { id: req.id, ok: true, data: { answers: ["yes"] } } },})
const res = await client.run("Do the whole demo.", { toolkit: tk })// res.toolCalls → ["add", "everything_echo", "skill", "reviewer_hello-world", "question"]// res.status → "done"Without a waitFor, the same run returns status: "pending" and hands you the Request to
answer later — a durable, resumable halt. That is the suspension layer; read it in full on
the suspension page.