Toolnexus 0.25.0

dotnet add package Toolnexus --version 0.25.0
                    
NuGet\Install-Package Toolnexus -Version 0.25.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Toolnexus" Version="0.25.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Toolnexus" Version="0.25.0" />
                    
Directory.Packages.props
<PackageReference Include="Toolnexus" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Toolnexus --version 0.25.0
                    
#r "nuget: Toolnexus, 0.25.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Toolnexus@0.25.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Toolnexus&version=0.25.0
                    
Install as a Cake Addin
#tool nuget:?package=Toolnexus&version=0.25.0
                    
Install as a Cake Tool

toolnexus

NuGet license

Build an agent in a few lines. Give a toolkit some tools — built-ins, an mcp.json, a skills/ folder, your own C# methods, HTTP endpoints, other agents — point a client at any LLM, call RunAsync(), and the tool-calling loop runs to an answer. Every source is unified behind one ITool interface and emitted in OpenAI / Anthropic / Gemini schema.

Right-sized. Not a framework (no builders to learn, no config to wade through), not a toy that falls over the moment you need streaming or a retry. Everything a real agent needs — the loop, hooks, streaming, retries, conversation memory — and nothing it doesn't.

The C#/.NET port of toolnexus — the same library, byte-identical, also in JavaScript, Python, Go, Java, Elixir, Clojure and Rust. Built on the official ModelContextProtocol SDK. Targets .NET 10.

Install

dotnet add package Toolnexus

Quick start

using Toolnexus;

// 1. a toolkit — the 10 built-in tools (bash/read/write/edit/grep/glob/…) are on by default
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options());

// 2. a client — point at any OpenAI- or Anthropic-style endpoint
var agent = LlmClient.Create(new LlmClient.Options
{
    BaseUrl = "https://openrouter.ai/api/v1",
    Style   = "openai",                       // or "anthropic"
    Model   = "anthropic/claude-3.5-sonnet",
    ApiKey  = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"),
});

// 3. run — tools called for you, looped to a final answer
var res = await agent.RunAsync("List the files in the current folder, then summarise the README.", tk);
Console.WriteLine(res.Text);

Add real tool sources by pointing at them:

await using var tk = await Toolkit.CreateAsync(new Toolkit.Options
{
    McpConfig = "mcp.json",
    SkillsDir = new[] { "skills" },
});

Simple judgments

A thin layer over Classifier (SPEC §8B): an ordered question list, a state, bands, and a gate that escalates (a §10 input Request) instead of deciding when unsure. The wire is unchanged.

var qs = new[] {
    Judge.Noul("is_appropriate", "Does `message_received` contain inappropriate language?"),
    Judge.Noul("does_this_help", "Does `message_received` help donkey kong win?"),
};
var state = Judge.State("You are Donkey Kong, you want to win.",
    new Dictionary<string, object?> { ["message_received"] = "jump off the stage now" });
var a = await Judge.AskAsync(classifier, state, qs);          // bands default 0.30 / 0.70
a["is_appropriate"].Band;                                      // "yes" | "no" | "uncertain"

var o = await Judge.GateAsync(classifier, state, qs, new Policy {
    Rules = new[] { Rule.Below("does_this_help", 0.3, "ignore") }, Default = "obey" });
// o.Escalated => o.Request is a §10 input Request

var ds = await classifier.EvaluateBatchAsync(states, Judge.Questions(qs)); // in order, fail-closed

Also: Judge.Context(ctx, message, extra), Judge.Choice / Judge.Score, answer.Value() / answer.Choice(), Tape (record by call name, replay offline), Classifier.FromRecorded(...), Question.ToWire(), public Decision.FromJson.

Batteries (§8B): ToolGuardClassifier, ToolRelevanceClassifier, SkillRelevanceClassifier, ToolResultFilterClassifier, IsCompleteClassifier, AgentRouterClassifier, ContentGuardClassifier, ModelRouterClassifier — e.g. new ToolGuardClassifier(classifier, new ToolGuardOptions { OnError = OnError.Closed }), then CheckAsync(...) standalone or AsHook(next) as a client hook. A BeforeLLM override may carry Model for that turn only (the opt-in ModelRouterClassifier sets it).

API key by host (BREAKING in 0.22.0)

With no ApiKey, the client reads ApiKeyEnv (the NAME of an env var) if set, else ONLY the env var of the BaseUrl host, over https: api.openai.com → OPENAI_API_KEY, api.anthropic.com → ANTHROPIC_API_KEY, openrouter.ai → OPENROUTER_API_KEY. A key is never sent to another host. A local server or proxy now needs one of the two:

var local = LlmClient.Create(new LlmClient.Options
{
    BaseUrl = "http://localhost:11434/v1", Style = "openai", Model = "llama3.2",
    ApiKey = "YOUR_KEY_HERE",            // or: ApiKeyEnv = "MY_PROXY_KEY"
});

API key by host · ListModelsAsync uses the same rule.

Prompt cache and tool tiers

var agent = LlmClient.Create(new LlmClient.Options
{
    BaseUrl = "https://api.anthropic.com", Style = "anthropic", Model = "claude-sonnet-4-5",
    PromptCache = new PromptCacheOptions { Key = "session-42" },             // cache_control / prompt_cache_key
    ToolTiers = new ToolTierOptions { Core = new[] { "read", "edit", "bash" } }, // the rest via tool_search / call_tool
});
// res.Usage.CacheReadTokens / CacheWriteTokens; agent.ToolTierPlan(tk); agent.ResetToolTiers(tk) at a new session

Only the core schemas are sent; the model loads the rest with tool_search and calls them by name or through call_tool (hooks still see the real tool), so the tools array and the cached prefix never change mid-session. Runnable: dotnet run --project examples/Toolnexus.Examples -- cache. Prompt cache · Tool tiers

Host additions (0.22.0)

  • Options.WaitForCtx((request, ct) => …) — a pending question ends with the turn.
  • McpSource.LoadAsync(cfg, new McpLoadOptions { OnServer = r => … }) — each MCP server reported as it is ready.
  • McpSource.ResourceTools() — opt-in list_mcp_resources / read_mcp_resource.
  • BeforeToolEvent.Hints (ReadOnly / Destructive / OpenWorld) on every builtin and MCP tool, for permission gates.
  • Opt-in builtins: ["background"] = new BackgroundShells() (bash_output, kill_shell; call CloseAll on shutdown), ["webSearch"] (your provider), ["notebookEdit"] = true.
  • grep context / before / after; read / grep report truncation in metadata.
  • Compaction.Options.OnCompact, Agent.AsToolWith(ctx => runtimeOptions) with RuntimeOptions.WaitFor, ToolContext.CallId, LlmClient.ListModelsAsync.
  • Not in C# yet: the ACP host hooks (Go only; openspec/changes/add-go-host-asks).

Media generation

Generate images, video and speech (plus transcription, voice cloning and talking-head video) from a JSON style instead of provider code. Async jobs are started and polled for you. The same verbs become agent tools (generate_image, generate_video, generate_speech, …), and whatever they produce lands in RunResult.files. examples/media-styles/a2e.json ships first and reads A2E_API_KEY at call time. Its result URLs expire in 3–30 days, so save what you keep. SPEC §7G; guide: https://muthuishere.github.io/toolnexus/media/.

var media = Media.Create(MediaStyle.Load("examples/media-styles/a2e.json"),
    new MediaOptions { OnProgress = p => Console.WriteLine($"{p.Verb} {p.Status}") });
var img = await media.ImageAsync(new ImageInput("a red fox in fresh snow") { Width = 1024, Height = 768 });
var vid = await media.VideoAsync(new VideoInput("the fox blinks") { Image = img.Files[0].Url, Seconds = 5 });

await using var tk = await Toolkit.CreateAsync(new Toolkit.Options { Media = media }); // generate_image, …
var res = await agent.RunAsync("Draw a fox, then animate it.", tk);
foreach (var f in res.Files) Console.WriteLine($"{f.From} {f.MimeType} {f.Url}");

Embeddable MCP server (SPEC §7C.1)

tk.McpHandler(cfg) returns an McpHttpHandler you mount on your own HTTP server, behind your own TLS, CORS and OAuth. ServeAsync is unchanged. Every policy is a hook:

var handler = tk.McpHandler(new McpHandlerConfig
{
    Authenticate = ex => VerifyBearer(ex.Header("Authorization")),      // throw => 401 (+ WwwAuthenticate)
    ListTools    = ctx => ToolsFor((User)ctx.Identity!),                 // per request; others are "tool not found"
    BeforeCall   = (ctx, tool, args) => Task.FromResult(tool.Annotations?.DestructiveHint == true
                       ? McpDecision.Confirm("Really delete?")          // elicitation on the same POST
                       : McpDecision.Allow()),
    OnCall       = ev => Log(ev.Name, ev.Decision, ev.ResultBytes),
});
// HttpListener host:  await handler.HandleAsync(httpListenerContext);
// any other server:   subclass McpHttpExchange and call handler.HandleAsync(exchange).
handler.NotifyToolsChanged(); // tools/list_changed on every open GET stream

Tools and hooks read the request through McpContext (Identity, Request, SessionId, Meta = params._meta); a tool uses McpContext.Current. Also: ServerInfo, resources/prompts hooks, pluggable SessionIds, Stateless, ProtocolVersions (2025-06-18 default, 2025-11-25 opt-in). ToolResult.Structured/Meta and ITool.OutputSchema/Annotations map to structuredContent, _meta, outputSchema and annotations. Runnable: dotnet run --project examples/Toolnexus.Examples -- mcphandler.

MCP 2026-07-28 on the same handler (SPEC §7C.2, opt-in)

List "2026-07-28" in ProtocolVersions (alone, or next to "2025-06-18" to serve both wires on one mount). The 2026 wire is stateless: server/discover, header validation, resultType + _meta serverInfo, and MRTR instead of elicitation — a Confirm decision or a §10 tool suspension (ToolResult.Pending) answers input_required, and the client's retry reaches the tool as ToolContext.Answer. Without the opt-in the handler is byte-identical to §7C.1.

var auth = new McpServerAuth                                   // OAuth 2.1 protected resource (RS256/JWKS by default)
{
    Issuer = "https://login.example",
    Scopes = new Dictionary<string, IReadOnlyList<string>> { ["*"] = ["mcp"], ["wipe"] = ["mcp:admin"] },
};
var handler = tk.McpHandler(new McpHandlerConfig
{
    ProtocolVersions = ["2026-07-28", "2025-06-18"],
    Authenticate     = auth.AuthenticateAsync,                 // identity = McpPrincipal; 401/403 + exact challenge
    ListSkills       = _ => Task.FromResult(McpHttpHandler.LoadMcpSkills("skills")), // skills/list, skills/get, skill:// reads
});
// route GET /.well-known/oauth-protected-resource[/mcp] to auth.Metadata() (or auth.MetadataAsync(listenerCtx))

McpServerAuth.Verify replaces the JWT verifier (opaque tokens, introspection). Bring your own authorization server: McpServerAuth verifies its tokens. The auto-approving dev authorization server the suite uses is test tooling (tests/Toolnexus.Tests/McpDevAuth.cs), not part of the library.

Agents via ACP

toolnexus ships no client per coding agent — three vendor-neutral pieces instead (SPEC §8C):

  • tk.AsMcpServer(cfg?) serves the toolkit for a local agent on http://127.0.0.1:<ephemeral>/mcp behind a fresh bearer token (Url, Token, Close()).
  • AcpClient.StartAsync(new AcpStartOptions { Command, Arguments, McpServers, SessionMeta, ClientCapsMeta, Model, OnPermission, … }) drives any ACP agent; McpServers entries are sent verbatim, so you build whichever entry your agent accepts.
  • InProcess.Options.GenerateStream = acp.GenerateStreamAsync streams the agent's answer (Text deltas, Thought, and its own ToolCall/ToolResult with Via = "acp").
await using var srv = tk.AsMcpServer();
using var acp = await AcpClient.StartAsync(new AcpStartOptions
{
    Command = "my-agent", Arguments = ["acp"],
    McpServers = [new Dictionary<string, object?>
    {
        ["type"] = "http", ["name"] = "toolnexus", ["url"] = srv.Url,
        ["headers"] = new[] { new Dictionary<string, object?> { ["name"] = "Authorization", ["value"] = "Bearer " + srv.Token } },
    }],
});
var client = InProcess.CreateClient(new InProcess.Options { Model = "acp", GenerateStream = acp.GenerateStreamAsync });

Documentation

Everything else — the full surface, with runnable examples — lives 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
API reference C#
Cookbook Zero to agent · MCP servers · Agent skills · Judge

Contract across all eight ports: SPEC.md.

MCP 2026-07-28 + OAuth

McpSource.LoadAsync / Toolkit.CreateAsync speak MCP 2026-07-28 first (SPEC §2): every server gets server/discover before anything else, over stdio or stateless Streamable HTTP, with no MCP SDK on that path. A server that is not a 2026-07-28 server (a JSON-RPC error, HTTP 400/404/405/5xx, or a silent stdio child) falls back to the frozen pre-2026 path, which still runs on the ModelContextProtocol SDK. Your config stays the same.

  • MRTR → §10: input_required rounds go through WaitFor. Without a WaitFor the tool returns {status:"pending"}; re-running it with the Answer resumes that same round. A tools/call is never re-sent after a lost connection, a timeout or a 5xx.
  • Skills over MCP: skills a server publishes through the skills extension join the one skill tool (base skill://<name>/).
  • Lazy tool search: set "toolSearch": true (and optionally "description") on a server. You then get <server>_search_tools and <server>_call_tool instead of every tool.
  • OAuth 2.1 (SPEC §2A): the per-server auth object takes clientId, clientSecret, tokenAuthMethod, clientIds, clientMetadataUrl, redirectUri, mode (auto|pause), omitRefreshResource and allowHttp. The client uses a pre-registered client id or a Client ID Metadata Document; dynamic client registration is never used. It does PKCE S256 + resource, iss/state checks, single-flight refresh and step-up. auto opens the browser and listens on loopback. pause sends a §10 authorization request; answer it with Data = { ["callbackUrl"] = "<redirected URL>" }.
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options
{
    McpConfig = "mcp.json",
    TokenStore = myStore,            // ITokenStore; default in-memory per load
    OpenUrl = url => OpenBrowser(url), // auto mode; default is the OS opener
    WaitFor = WaitFor,               // MRTR input + pause-mode authorization
});

On the frozen fallback, the bearer goes onto the SDK's HTTP transport through a delegating handler. A 401 there runs the same flow once. Not yet on the 2026 path: list_mcp_resources / read_mcp_resource (ResourceTools() only sees fallback servers), progress notifications, and the server side (serve).

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.25.0 0 11/5/2026
0.24.0 59 11/3/2026
0.23.0 80 11/2/2026
0.22.0 77 11/1/2026
0.21.0 168 9/28/2026
0.20.0 113 9/23/2026
0.19.0 108 9/22/2026
0.18.1 106 9/21/2026
0.18.0 106 9/21/2026
0.17.0 125 9/2/2026
0.16.0 171 8/17/2026
0.15.0 128 8/17/2026
0.14.0 123 8/15/2026
0.13.0 137 8/2/2026
0.12.0 133 7/30/2026
0.10.0 124 7/19/2026
0.9.5 126 7/15/2026
0.9.4 140 7/14/2026
0.9.3 135 7/14/2026
0.9.0 139 7/13/2026
Loading failed