API Reference
The same API, six languages. Every entry below shows its exact per-language equivalent in synced tabs — pick your language once and it sticks across the page. Per-port index pages: JavaScript · Python · Go · Java · C# · Elixir.
Build a toolkit
Section titled “Build a toolkit”The one aggregator: MCP tools + skills + built-ins + A2A agents + your own tools behind one Tool
interface.
import { createToolkit } from "toolnexus"
const tk = await createToolkit({ mcpConfig: "./mcp.json", // path | object skillsDir: "./skills", // string | string[] skills: [/* SkillDef[] */], // data skills skillProvider: async () => [/* SkillDef[] */], // lazy, resolved once skillsFilter: { deploy: true }, // per-agent allowlist skillSampleLimit: 0, // 0=10, n=cap, -1=off builtins: true, agents: [/* Agent[] */], extraTools: [/* Tool[] */],})from toolnexus import create_toolkit
tk = await create_toolkit( mcp_config="./mcp.json", skills_dir="./skills", skills=[...], # SkillDef list skill_provider=lambda: [...], # sync or async skills_filter={"deploy": True}, skill_sample_limit=0, builtins=True, agents=[...], extra_tools=[...],)tk, _ := toolnexus.CreateToolkit(ctx, toolnexus.Options{ McpConfig: "./mcp.json", SkillsDir: []string{"./skills"}, Skills: []toolnexus.SkillDef{ /* … */ }, SkillProvider: func(ctx context.Context) ([]toolnexus.SkillDef, error) { return nil, nil }, SkillsFilter: map[string]bool{"deploy": true}, SkillSampleLimit: 0, Builtins: true,})Toolkit tk = Toolkit.create(new Toolkit.Options() .mcpConfig("./mcp.json") .skillsDir("./skills") .skills(List.of(/* SkillDef */)) .skillProvider(() -> List.of(/* SkillDef */)) .skillsFilter(Map.of("deploy", true)) .skillSampleLimit(0) .builtins(true));await using var tk = await Toolkit.CreateAsync(new Toolkit.Options{ McpConfig = "./mcp.json", SkillsDir = new List<string> { "./skills" }, Skills = new List<SkillSource.SkillDef> { /* … */ }, SkillProvider = () => new List<SkillSource.SkillDef>(), SkillsFilter = new Dictionary<string, bool> { ["deploy"] = true }, SkillSampleLimit = 0, Builtins = true,});{:ok, tk} = Toolnexus.create_toolkit( mcp_config: "./mcp.json", skills_dir: ["./skills"], skills: [%{name: "…", content: "…"}], # data skills skill_provider: fn -> [] end, # lazy, resolved once skills_filter: %{"deploy" => true}, # per-agent allowlist skill_sample_limit: 0, # 0=10, n=cap, -1=off builtins: true, agents: [], extra_tools: [] )Skills — load, supply as data, filter, inventory
Section titled “Skills — load, supply as data, filter, inventory”Directories, in-memory SkillDefs, and a lazy provider all compose (first-wins dedupe). See the
Agent skills guide for the concepts; this is the exact surface.
SkillDef — a skill as data
Section titled “SkillDef — a skill as data”interface SkillDef { name: string description?: string content: string resources?: string[] // logical names; nil ⇒ instruction-only base?: string // logical base; default "skill://<name>/"}@dataclassclass SkillDef: name: str content: str description: str | None = None resources: list[str] | None = None base: str | None = Nonetype SkillDef struct { Name string Description string Content string Resources []string Base string}new SkillSource.SkillDef(name, description, content); // instruction-onlynew SkillSource.SkillDef(name, description, content, resources, base);new SkillSource.SkillDef(Name, Description, Content, Resources: null, Base: null); // record# a data skill is a plain map; keys mirror the struct%{ name: "…", description: "…", content: "…", resources: nil, # logical names; nil ⇒ instruction-only base: nil # logical base; default "skill://<name>/"}List & validate — ListSkills
Section titled “List & validate — ListSkills”Discover + validate without building a toolkit. Returns parsed skills plus typed skip reasons:
missing-name · malformed-frontmatter · duplicate-name · unreadable.
import { listSkills } from "toolnexus"
const inv = listSkills({ dirs: "./skills" })// inv.skills: SkillInfo[] inv.skipped: { location, reason }[]from toolnexus import list_skills
inv = list_skills("./skills")# inv.skills: list[SkillInfo] inv.skipped: list[SkillSkip]inv := toolnexus.ListSkills(toolnexus.LoadSkillsOptions{Dirs: []string{"./skills"}})// inv.Skills []SkillInfo inv.Skipped []SkillSkipvar inv = SkillSource.listSkills(new SkillSource.LoadOptions().dirs("./skills"));// inv.skills inv.skipped (SkillSkip.reason)var inv = SkillSource.ListSkills(new SkillSource.LoadOptions { Dirs = new[] { "./skills" } });// inv.Skills inv.Skipped (SkillSkip.Reason)inv = Toolnexus.Skill.list("./skills")# inv.skills inv.skipped (%{location, reason})Load MCP servers
Section titled “Load MCP servers”Read an mcp.json, connect every server (stdio + streamable-HTTP), expose each tool as a Tool.
import { loadMcp } from "toolnexus"const mcp = await loadMcp("./mcp.json") // mcp.tools: Tool[]from toolnexus import load_mcpmcp = await load_mcp("./mcp.json")mcp, _ := toolnexus.LoadMcp("./mcp.json")McpSource mcp = McpSource.load("./mcp.json");var mcp = await McpSource.LoadAsync("./mcp.json");{:ok, mcp} = Toolnexus.Mcp.load("./mcp.json") # mcp.toolsAdapters — emit provider schema
Section titled “Adapters — emit provider schema”Turn a toolkit into the tool-schema shape each provider expects.
import { toOpenAI, toAnthropic, toGemini } from "toolnexus"const tools = toOpenAI(tk) // toAnthropic(tk) · toGemini(tk)from toolnexus import to_openai, to_anthropic, to_geminitools = to_openai(tk)tools := tk.ToOpenAI() // tk.ToAnthropic() · tk.ToGemini()var tools = Adapters.toOpenAI(tk); // toAnthropic · toGeminivar tools = Adapters.ToOpenAI(tk); // ToAnthropic · ToGeminitools = Toolnexus.Adapters.to_openai(tk.tools) # to_anthropic · to_geminiBuild a client (the tool-calling loop)
Section titled “Build a client (the tool-calling loop)”import { createClient } from "toolnexus"const client = createClient({ model: "gpt-5", baseURL: "…", apiKey: "…" })const res = await client.ask("…", tk)from toolnexus import create_clientclient = create_client(model="gpt-5", base_url="…", api_key="…")res = await client.ask("…", tk)client := toolnexus.CreateClient(toolnexus.ClientOptions{ Model: "gpt-5", BaseURL: "…", APIKey: "…",})res, _ := client.Ask(ctx, "…", tk, "")Client client = Client.create(new Client.Options() .model("gpt-5") .baseUrl("…") .apiKey("…"));var res = client.ask("…", tk);var client = Client.Create(new Client.Options { Model = "gpt-5", BaseUrl = "…", ApiKey = "…",});var res = await client.AskAsync("…", tk);client = Toolnexus.Client.create(style: "openai", model: "gpt-5", base_url: "…", api_key: "…")res = Toolnexus.Client.run(client, "…", tk)Your own functions — native tools
Section titled “Your own functions — native tools”Wrap a function as a Tool (source: "native"). Pass them via extraTools or hand them to an
agent’s uses.tools. See the Native tools guide.
import { defineTool } from "toolnexus"
const add = defineTool({ name: "add", description: "Add two numbers", run: (a) => String(Number(a.x) + Number(a.y)),})// decorator sugar: @tool(description, { name?, inputSchema? }) + collectTools(obj)from toolnexus import tool # or define_tool(...)
@tool # schema inferred from type hints + docstringdef add(a: int, b: int) -> str: """Add two numbers.""" return str(a + b)add := toolnexus.NativeTool("add", "Add two numbers", nil, func(ctx context.Context, a map[string]any) (string, error) { return fmt.Sprint(a["x"], a["y"]), nil })// or NativeToolReflect[T] — schema derived from a struct's json tagsTool add = NativeTool.of("add", "Add two numbers", schema, args -> String.valueOf((int) args.get("a") + (int) args.get("b")));// overload with (args, ToolContext) for suspension-aware toolsvar add = NativeTool.Of("add", "Add two numbers", inputSchema: null, args => (object?)((int)args["a"]! + (int)args["b"]!));// also NativeTool.OfAsync(...) and an (args, ToolContext) overloadadd = Toolnexus.Native.define_tool( name: "add", description: "Add two numbers", execute: fn args -> to_string(args["a"] + args["b"]) end)# execute may be fn args -> end or fn args, ctx -> endHTTP / REST tools
Section titled “HTTP / REST tools”Turn any endpoint into a Tool (source: "http"). {placeholder} path substitution, ${ENV_VAR}
header expansion (never logged), resultMode text (default) · json · status+text. See the
HTTP tools guide.
import { httpTool } from "toolnexus"
const weather = httpTool({ name: "weather", description: "Current weather", method: "GET", url: "https://api.example.com/weather/{city}",}) // timeout ms (default 30000)from toolnexus import http_tool
weather = http_tool(name="weather", description="Current weather", method="GET", url="https://api.example.com/weather/{city}")# timeout in SECONDS hereweather := toolnexus.HTTPTool(toolnexus.HTTPToolOptions{ Name: "weather", Description: "Current weather", Method: "GET", URL: "https://api.example.com/weather/{city}",}) // Timeout ms (0 ⇒ 30000)var o = new HttpTool.Options();o.name = "weather"; o.description = "Current weather";o.method = "GET"; o.url = "https://api.example.com/weather/{city}";Tool weather = HttpTool.of(o); // timeout Long msvar weather = HttpTool.Of(new HttpTool.Options { Name = "weather", Description = "Current weather", Method = "GET", Url = "https://api.example.com/weather/{city}",}); // Timeout long? msweather = Toolnexus.Http.tool( name: "weather", description: "Current weather", method: "GET", url: "https://api.example.com/weather/{city}")# note: Toolnexus.Http.tool/1 (not http_tool); :timeout in msBuilt-in tools
Section titled “Built-in tools”builtins (toolkit option, default on) ships the same ten tools in every port, source: "builtin", identical names:
bash · read · write · edit · grep · glob · webfetch · question · apply_patch · todowritequestion suspends through the human loop (a kind: "question" Request). The toggle accepts
true | false | { enabled?, disabled?, tools?: { <name>: false } } — disabled: true wins, and a
tools entry set to false drops that one built-in. See the Built-in tools guide.
const tk = await createToolkit({ builtins: { tools: { bash: false } }, // all built-ins except bash})tk = await create_toolkit(builtins={"tools": {"bash": False}})tk, _ := toolnexus.CreateToolkit(ctx, toolnexus.Options{ Builtins: map[string]any{"tools": map[string]any{"bash": false}},})Toolkit tk = Toolkit.create(new Toolkit.Options() .builtins(Map.of("tools", Map.of("bash", false))));await using var tk = await Toolkit.CreateAsync(new Toolkit.Options { Builtins = new Dictionary<string, object?> { ["tools"] = new Dictionary<string, object?> { ["bash"] = false } },});{:ok, tk} = Toolnexus.create_toolkit(builtins: %{"tools" => %{"bash" => false}})Sub-agents & teams
Section titled “Sub-agents & teams”An Agent is a Tool: a soul (system prompt) × a scoped toolkit × the client loop. team is the
only set of targets the built-in task tool can delegate to — no team ⇒ no task tool. asTool
turns an agent into a Tool for extraTools; composeSoul builds a soul from a directory
(AGENTS.md + sections). Result status ∈ done · pending · incomplete · interrupted · closed · timeout · error. An agent (or the runtime) also accepts hooks and onMetric — the same two
lifecycle seams a hand-built client takes, forwarded verbatim; an agent’s value replaces the
runtime-wide one, which is how a compactor gives each agent its own context budget. See the
Sub-agents & teams guide.
import { agents } from "toolnexus"
const explore = agents.agent("explore", { does: "read-only research", uses: { tools: [lookup] } })const coder = agents.agent("coder", { does: "implements changes", soulFile: "./AGENTS.md", team: [explore], budget: { maxTokens: 10_000 },})const r = await coder.run("fix the failing test", { llm: { baseUrl: "…", style: "openai", model: "gpt-4o-mini" },})// r.status · r.text · r.totalTokens ; delegate as a tool: explore.asTool({ llm })from toolnexus.agents import agent, Budget
explore = agent("explore", does="read-only research", uses={"tools": [lookup]})coder = agent("coder", does="implements changes", soul_file="./AGENTS.md", team=[explore], budget=Budget(max_tokens=10_000))r = await coder.run("fix the failing test", llm={"base_url": "…", "style": "openai", "model": "gpt-4o-mini"})# r.status · r.text · r.total_tokens ; explore.as_tool(llm=llm)import "github.com/muthuishere/toolnexus/golang/agents"
explore := agents.New("explore", agents.Spec{Does: "read-only research", Tools: []toolnexus.Tool{lookup}})coder := agents.New("coder", agents.Spec{ Does: "implements changes", SoulFile: "./AGENTS.md", Team: []*agents.Agent{explore}, Budget: &agents.Budget{MaxTokens: 10_000}})r, _ := coder.Run(agents.Options{LLM: &agents.LLMOptions{ BaseURL: "…", Style: toolnexus.StyleOpenAI, Model: "gpt-4o-mini"}}, "fix the failing test")// r.Status · r.Text · r.TotalTokens ; explore.AsTool(agents.Options{LLM: llm})import static io.github.muthuishere.toolnexus.agents.Agents.agent;
var explore = agent("explore", new Agents.AgentSpec().does("read-only research").tools(lookup));var coder = agent("coder", new Agents.AgentSpec() .does("implements changes").soulFile(Path.of("AGENTS.md")) .team(explore).budget(new Budget().maxTokens(10_000)));TaskResult r = coder.run(new RuntimeOptions() .baseUrl("…").style("openai").defaultModel("gpt-4o-mini"), "fix the failing test");// r.status() · r.text() · r.totalTokens() ; explore.asTool(rtOpts)using Toolnexus.Agents;
var explore = new Agent("explore", new AgentSpec { Does = "read-only research", Uses = new List<ITool> { lookup } });var coder = new Agent("coder", new AgentSpec { Does = "implements changes", SoulFile = "./AGENTS.md", Team = new List<Agent> { explore }, Budget = new Budget { MaxTokens = 10_000 } });var r = await coder.RunAsync(new RuntimeOptions { BaseUrl = "…", Style = "openai", Model = "gpt-4o-mini" }, "fix the failing test");// r.Status · r.Text · r.TotalTokens ; explore.AsTool(rtOpts)alias Toolnexus.Agents
explore = Agents.agent("explore", does: "read-only research", uses: %{tools: [lookup]})coder = Agents.agent("coder", does: "implements changes", soul_file: "AGENTS.md", team: [explore], budget: %{max_tokens: 10_000})r = Agents.run(coder, [llm: %{base_url: "…", style: "openai", model: "gpt-4o-mini"}], "fix the failing test")# r.status · r.text · r.total_tokens ; Agents.as_tool(explore, rt_opts)A2A — serve inbound, consume remote agents
Section titled “A2A — serve inbound, consume remote agents”Two directions of the same protocol (JSON-RPC 2.0; card at GET /.well-known/agent-card.json).
Serve exposes a toolkit as a remote A2A agent; remote agents pull a peer’s card and expose
its skills as <agent>_<skill> tools (source: "a2a"). See the A2A guide.
Serve — expose a toolkit
Section titled “Serve — expose a toolkit”const handle = await tk.serve("127.0.0.1:0", { client: llm, a2a: { name: "my-agent", store: "memory" }, // store: "memory" | "file:<dir>" | TaskStore})// handle.url speaks A2A ; await handle.stop()from toolnexus import A2AConfig
handle = await tk.serve("127.0.0.1:0", client=llm, a2a=A2AConfig(name="my-agent", store="memory"))# handle.url ; await handle.stop()handle, _ := tk.Serve("127.0.0.1:0", toolnexus.ServeOptions{ Client: llm, A2A: &toolnexus.A2AConfig{Name: "my-agent", Store: "memory"},})// handle.URL ; handle.Stop()A2AServer.ServeHandle handle = tk.serve("127.0.0.1:0", new Toolkit.ServeOptions() .client(llm) .a2a(new A2AServer.A2AConfig().name("my-agent").store("memory")));var handle = await tk.ServeAsync("127.0.0.1:0", new Toolkit.ServeOptions { Client = llm, A2A = new A2AConfig { Name = "my-agent", Store = "memory" },});handle = Toolnexus.Toolkit.serve(tk, "127.0.0.1:0", client: llm, a2a: %{name: "my-agent", store: "memory"})# handle.url ; Toolnexus.Serve.stop(handle)Remote agents — consume a peer
Section titled “Remote agents — consume a peer”const tk = await createToolkit({ agents: [agent({ card: "https://peer.example.com/.well-known/agent-card.json" })],})await tk.addAgent("https://other.example.com/.well-known/agent-card.json")from toolnexus import create_toolkit, agent
tk = await create_toolkit( agents=[agent("https://peer.example.com/.well-known/agent-card.json")])await tk.add_agent("https://other.example.com/.well-known/agent-card.json")tk, _ := toolnexus.CreateToolkit(ctx, toolnexus.Options{ Agents: []toolnexus.Agent{{Card: "https://peer.example.com/.well-known/agent-card.json"}},})tk.AddAgent(ctx, "https://other.example.com/.well-known/agent-card.json", nil)Toolkit tk = Toolkit.create(new Toolkit.Options() .agents(List.of(A2A.agent("https://peer.example.com/.well-known/agent-card.json"))));tk.addAgent("https://other.example.com/.well-known/agent-card.json");await using var tk = await Toolkit.CreateAsync(new Toolkit.Options { Agents = new List<Agent> { new() { Card = "https://peer.example.com/.well-known/agent-card.json" } },});await tk.AddAgentAsync("https://other.example.com/.well-known/agent-card.json");{:ok, tk} = Toolnexus.create_toolkit( agents: [Toolnexus.A2A.agent(card: "https://peer.example.com/.well-known/agent-card.json")])Toolnexus.Toolkit.add_agent(tk, "https://other.example.com/.well-known/agent-card.json")Suspension & the human loop
Section titled “Suspension & the human loop”waitFor is the host resolver (Request) → Answer. A tool suspends by returning a pending
result (isError: true, metadata.pending = Request); the loop calls waitFor, then re-runs the
tool with Answer on the tool context. Request { id, kind, prompt, url?, data?, expiresAt? } ·
Answer { id, ok, data?, reason? }. This is also the bridge for MCP elicitation and the built-in
question tool. See the Suspension guide.
import { pending, authRequired, pendingOf } from "toolnexus"
const tk = await createToolkit({ waitFor: async (req) => ({ id: req.id, ok: true, data: { answer: "…" } }),})// inside a tool: return pending({ kind: "input", prompt: "…" }) // or authRequired(url)from toolnexus import pending, auth_required, Answer
tk = await create_toolkit( wait_for=lambda req: Answer(id=req.id, ok=True, data={"answer": "…"}))# inside a tool: return pending(kind="input", prompt="…")tk, _ := toolnexus.CreateToolkit(ctx, toolnexus.Options{ WaitFor: func(req toolnexus.Request) (toolnexus.Answer, error) { return toolnexus.Answer{ID: req.ID, Ok: true, Data: map[string]any{"answer": "…"}}, nil },})// inside a tool: return toolnexus.Pending(toolnexus.Request{Kind: "input", Prompt: "…"})Toolkit tk = Toolkit.create(new Toolkit.Options() .waitFor(req -> new Answer(req.id(), true, Map.of("answer", "…"), null)));// inside a tool: return ToolResult.pending(new Request(id, "input", "…"));await using var tk = await Toolkit.CreateAsync(new Toolkit.Options() .WithWaitFor(req => Task.FromResult( new Answer { Id = req.Id, Ok = true, Data = new() { ["answer"] = "…" } })));// inside a tool: return ToolResult.Pending(new Request { Kind = "input", Prompt = "…" });{:ok, tk} = Toolnexus.create_toolkit( wait_for: fn req -> %Toolnexus.Answer{id: req.id, ok: true, data: %{"answer" => "…"}} end)# a tool suspends by returning %Toolnexus.ToolResult{is_error: true,# metadata: %{pending: %Toolnexus.Request{kind: "input", prompt: "…"}}}