Adapters.ToAnthropic
C# · package Toolnexus · SPEC §4 · Adapters.cs
public static List<Dictionary<string, object?>> ToAnthropic(IEnumerable<ITool> tools)Turns tools into the tools array an Anthropic Messages request expects. Same tools, same
execution — only the schema envelope differs from OpenAI.
The Anthropic shape is the flattest of the three: one entry per tool, carrying name,
description and input_schema, with no type: "function" wrapper around it.
When to use it
Section titled “When to use it”When you are calling POST /v1/messages yourself — through the official SDK, a raw HttpClient,
or a gateway that speaks the Anthropic wire format (Bedrock, Vertex) — and need schema to put in
the request body.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. One tool to Anthropic schema
Section titled “1. One tool to Anthropic schema”using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var schema = Adapters.ToAnthropic(new ITool[] { weather });
if (schema.Count != 1) throw new Exception("expected 1 entry");if (schema[0]["name"] as string != "get_weather") throw new Exception("name");if (schema[0]["description"] as string != "Current weather for a city") throw new Exception("description");
// Flat: no "type"/"function" wrapper, and the schema key is input_schema.if (schema[0].ContainsKey("function")) throw new Exception("there is no function wrapper here");if (!schema[0].ContainsKey("input_schema")) throw new Exception("input_schema");
Console.WriteLine($"ok: {schema[0]["name"]}");ITool.InputSchema becomes input_schema — snake_case, because that is what the Anthropic wire
format uses. Compare with OpenAI, where the same dictionary is nested under
function.parameters.
2. Feeding it straight into a Messages request body
Section titled “2. Feeding it straight into a Messages request body”The output is plain dictionaries, so System.Text.Json serializes it with no converter.
using System.Text.Json;using Toolnexus;
ITool Mk(string name, string desc) => NativeTool.Of( name, desc, new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?>() }, (IDictionary<string, object?> a) => name);
var tools = new[] { Mk("search", "Search the docs"), Mk("ping", "Health check") };
var body = new Dictionary<string, object?>{ ["model"] = "claude-sonnet-4-5", ["max_tokens"] = 1024, ["messages"] = new[] { new Dictionary<string, object?> { ["role"] = "user", ["content"] = "search for adapters" } }, ["tools"] = Adapters.ToAnthropic(tools),};
var entries = (List<Dictionary<string, object?>>)body["tools"]!;if (entries.Count != 2) throw new Exception("expected 2 tools");
// Order is preserved.var names = entries.Select(e => (string)e["name"]!).ToList();if (names[0] != "search" || names[1] != "ping") throw new Exception($"order: {string.Join(",", names)}");
var json = JsonSerializer.Serialize(body);if (!json.Contains("\"input_schema\"")) throw new Exception("expected input_schema on the wire");
Console.WriteLine($"ok: {string.Join(", ", names)}");3. Round-tripping a tool_use block back to the tool
Section titled “3. Round-tripping a tool_use block back to the tool”Schema out, tool call in. The name you advertised is the name the model returns.
using System.Text.Json;using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var tools = new ITool[] { weather };if (Adapters.ToAnthropic(tools)[0]["name"] as string != "get_weather") throw new Exception("advertised name");
// What a model would send back: a content block whose `input` is a real JSON OBJECT.const string block = """{"type":"tool_use","id":"toolu_01","name":"get_weather","input":{"city":"Chennai"}}""";
var parsed = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(block)!;var calledName = parsed["name"].GetString()!;// NB: `args` is already the implicit parameter of a top-level program — pick another name.var callArgs = parsed["input"].EnumerateObject() .ToDictionary(p => p.Name, p => (object?)p.Value.ToString());
var called = tools.FirstOrDefault(t => t.Name == calledName) ?? throw new Exception("the advertised name should resolve back to the tool");
var res = await called.ExecuteAsync(callArgs);if (res.IsError || res.Output != "sunny in Chennai") throw new Exception(res.Output);
// An empty tool list is valid — it just means "no tools this turn".if (Adapters.ToAnthropic(Array.Empty<ITool>()).Count != 0) throw new Exception("expected empty");
Console.WriteLine($"ok: {calledName} -> {res.Output}");| Path | From | Notes |
|---|---|---|
[]["name"] |
ITool.Name |
What the model calls back with. |
[]["description"] |
ITool.Description |
|
[]["input_schema"] |
ITool.InputSchema |
Renamed — InputSchema → input_schema. |
There is no wrapper key and no wrapper entry: an empty tool list produces an empty list.
See also
Section titled “See also”Adapters.ToOpenAI·Adapters.ToGeminiToolkit.CreateAsync— the toolkit exposes this applied to its own toolsLlmClient.Create— calls the adapter for youITool— the four members the adapter reads