We wrote one agent SDK and ported it, by hand, to eight languages — JS, Python, Go, Java, C#, Elixir, Clojure, Rust — until the same conformance suite passes byte-identical output in every one of them.
Point toolnexus at an mcp.json and a skills/ folder and you get a working agent: the
tool-calling loop, skills injection, six unified tool sources, and conversation memory — all
included. Vendor-neutral, byte-identical across JavaScript · Python · Go · Java · C# · Elixir ·
Clojure — the Clojure port is one .cljc tree that runs on both the JVM and cljgo, held
to the same full tier as the other six and published to Clojars like the rest.
Right-sized. Not a framework — no builders, advisors, runnables, or config to wade through. Not a toy that falls over the moment you need streaming or a retry. Exactly what a real agent needs — MCP, skills, native + HTTP + built-in tools, remote A2A agents, in-process sub-agents, the loop, hooks, streaming, retries, memory — and nothing it doesn't.
npm i toolnexus # JS / TypeScript
pip install toolnexus # Python
go get github.com/muthuishere/toolnexus/golang # Go
dotnet add package Toolnexus # C#
{:toolnexus, "~> 0.23"} # Elixir (mix.exs deps)
# Java (Maven): io.github.muthuishere:toolnexus:0.23.0
# Clojure (deps.edn): net.clojars.muthuishere/toolnexus {:mvn/version "0.23.0"} — JVM and cljgo
cargo add toolnexus # Rust (crates.io)
The insight (borrowed from opencode): MCP server
tools, agent skills, your own functions, remote HTTP endpoints, the built-in shell/file
tools, remote A2A agents, and in-process sub-agents are all the same thing to an LLM — a
named, described, schema'd callable. toolnexus unifies every tool source behind one Tool
interface and drives any model with them.
1. Point at an mcp.json — the standard Claude-desktop-style config:
{
"mcpServers": {
"fs": { "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/data"] },
"acme": { "type": "remote", "url": "https://api.acme.com/mcp",
"headers": { "Authorization": "Bearer ${ACME_TOKEN}" } }
}
}
2. Point at a skills/ folder — **/SKILL.md files with YAML frontmatter, loaded on demand
through one skill tool (progressive disclosure).
3. Run.
import { createToolkit, createClient } from "toolnexus"
const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
const agent = createClient({ baseUrl: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini" })
const { text } = await agent.run("Refund order 4021 using the policy.", { toolkit: tk })
The key above comes from OPENROUTER_API_KEY, because the host is openrouter.ai. Since
0.22.0 a key is only ever sent to the host it belongs to: with no apiKey, the client reads
only the env var matching the baseUrl host (openrouter.ai → OPENROUTER_API_KEY,
api.openai.com → OPENAI_API_KEY, api.anthropic.com → ANTHROPIC_API_KEY, https only).
A local server or a proxy needs apiKey or apiKeyEnv (the name of the env var to read):
createClient({ baseUrl: "http://localhost:11434/v1", style: "openai", model: "llama3", apiKey: "unused" })
createClient({ baseUrl: "https://llm.internal/v1", style: "openai", model: "m", apiKeyEnv: "INTERNAL_LLM_KEY" })
The same three steps, in the same shape, in every port — see your language's API reference below.
promptCache marks the stable prefix for the provider
(Anthropic cache breakpoints, OpenAI prompt_cache_key) and usage reports
cacheReadTokens / cacheWriteTokens. toolTiers sends only your core tool schemas and
lets the model load the rest through tool_search / call_tool, so the tools array — and
the cached prefix — never changes mid-session.
Recipe.waitFor, per-server MCP readiness (onServer), MCP resource
tools, tool hints (readOnly / destructive / openWorld) on beforeTool for permission
gates, opt-in builtins (background shells, websearch, notebook_edit), grep context,
compaction notices, asToolWith for subagents under the parent's gates, callId on the tool
context, and listModels.
Recipe.OnPermission, OnUpdate, GenerateCtx,
GenerateStream). Recipe.Per-port spellings are in CHANGELOG.md and each port's API reference.
| language | package | API reference |
|---|---|---|
| JavaScript / TypeScript | toolnexus (npm) | api/javascript |
| Python | toolnexus (PyPI) | api/python |
| Go | github.com/muthuishere/toolnexus/golang | api/go |
| Java | io.github.muthuishere:toolnexus | api/java |
| C# / .NET | Toolnexus (NuGet) | api/csharp |
| Elixir | toolnexus (Hex) | api/elixir |
| Clojure | net.clojars.muthuishere/toolnexus | api/clojure |
| Rust | toolnexus (crates.io) · rust/ | api/rust |
Each port's own README covers install and a quick start; everything beyond that is on the docs site.
| Start here | Quickstart · Concepts · Install |
| Tool sources | MCP · Skills · Native · HTTP · Built-ins · A2A |
| The loop | Streaming · Memory · Suspension · Resilience · Observability |
| Agents | Sub-agents & teams · Personas · Typed decisions (Classifier) |
| Cookbook | Zero to agent · MCP servers · Agent skills · Prompt cache & tool tiers · API key by host · Host hooks · Judge |
| Numbers | Performance · Comparison · Judge, live |
SPEC.md is the cross-language contract — §0 is the one-page conformance suite every
port must satisfy. A behaviour change lands in all eight ports or it is not done; the shared
fixtures in examples/ are what "identical" is measured against.
Changes go through OpenSpec before code. See CLAUDE.md for the
workflow, CHANGELOG.md for what shipped, and PUBLISHING.md
for how each port is released.
MIT — see LICENSE.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |