Schema-first argument binding and repair for LLM tool calls.
toolargs is the argument-contract layer between an LLM and your MCP/tool implementation.
It solves a common production problem: the model sees a schema, but the arguments it sends back are often not the exact Go/API shape your tool expects. Without a shared layer, every tool ends up writing its own field-name fixes, type coercion, schema rendering, validation, logging, and retry feedback.
With toolargs, a tool author defines one Go struct, registers it once at startup, exposes schema from the compiled contract, and binds runtime arguments through the same contract.
LLM tool calls fail in ways that are easy to underestimate:
- The model sends the wrong field name, such as
phoneinstead ofphoneNumber. - The model changes casing or separators, such as
ticket_idinstead ofticketId. - The model sends the wrong JSON type, such as
"8"instead of8. - The model omits required fields or sends extra meaningless fields.
- The model sends an ambiguous field, such as
phonewhen bothphoneNumberandphoneNumexist. - Tool authors duplicate repair logic across tools, and each copy behaves slightly differently.
- Schema generation and argument binding drift apart when they are implemented separately.
- Bad tool definitions are discovered only after a model call reaches production.
These are generic LLM argument-contract problems, not business-domain problems. toolargs keeps them out of each individual MCP tool.
Your schema says:
{"phoneNumber": "string"}The model sends:
{"phone": "13120057004"}Without toolargs, each tool has to decide whether phone means phoneNumber, reject it, or silently ignore it. With toolargs, deterministic matching can bind phone to phoneNumber when it is the only safe candidate, and records the repair in BindReport.
Your tool expects:
{"priority": 3}The model sends:
{"priority": "3"}toolargs can coerce safe scalar values and report the conversion. Tool logic receives the typed Go value instead of reimplementing conversion in every handler.
Your tool has both:
{"phoneNumber": "string", "phoneNum": "string"}The model sends:
{"phone": "13120057004"}toolargs does not guess. It returns a retryable argument error that tells the model to use phoneNumber or phoneNum.
The model sends mixed-quality arguments:
{"phone": "13120057004", "priority": "3", "extra": "ignored?"}toolargs analyzes the full argument object, matches fields, coerces safe values, and writes the final typed Go struct when the input is acceptable. If the input is not acceptable, it returns a structured error that can be sent back to the model, including the problematic path, expected field or type, received value, examples, and a suggested fix. At the same time, BindReport gives the application enough detail to log what was repaired or rejected, making later debugging and prompt/tool-schema tuning much easier.
For payment, deletion, permission, or other high-risk tools, silent field-name repair may be unacceptable. Register the tool with MatchingModeExact, and toolargs will only accept fields that exactly match the schema.
Invalid examples, unsupported field types, required groups that reference missing fields, schema conflicts, and contradictory matching options are caught during Register. The service fails fast instead of discovering the issue after an LLM call reaches the tool.
- Uses one registered Go struct as the single source of truth for model-facing schema and runtime binding.
- Validates predictable setup problems at startup: tags, examples, required groups, schema conflicts, field-name collision warnings, unknown-field policy, and matching-mode conflicts.
- Repairs deterministic model mistakes by default: exact, normalized, and derived-key field names, plus safe type coercion.
- Analyzes arguments and assigns typed Go values when the input is acceptable.
- Lets high-risk tools choose strict exact field matching instead of automatic field-name repair.
- Returns clear model-readable feedback when the input is not acceptable.
- Produces
BindReportfor logs so teams can see exactly which fields were repaired, converted, ignored, rejected, or left ambiguous.
toolargs does not implement MCP transport, choose tools, reroute between tools, call an LLM to repair arguments, or own downstream business validation. It only handles the generic LLM tool-argument contract layer.
go get github.com/gpencil/toolargs
package main import ( "errors" "log" "github.com/gpencil/toolargs" ) type TicketArgs struct { PhoneNumber string `json:"phoneNumber" toolargs:"required,desc=user phone number,example=13120057004"` Priority int `json:"priority" toolargs:"required,desc=ticket priority,example=3"` } func main() { registry := toolargs.NewRegistry() err := toolargs.Register[TicketArgs](registry, toolargs.Tool{ Name: "query_tickets", Description: "Query support tickets by user phone number.", }) if err != nil { log.Fatal(err) } schema, err := registry.Schema("query_tickets") if err != nil { log.Fatal(err) } _ = schema // expose this schema to the upstream model rawArgs := []byte(`{"phone":"13120057004","priority":"3"}`) var args TicketArgs report, err := toolargs.BindWithReport[TicketArgs](registry, "query_tickets", rawArgs, &args) if err != nil { var uncertain *toolargs.UncertainMatchError if errors.As(err, &uncertain) { log.Printf("arguments bound with uncertain repairs: %+v", uncertain.Repairs) // args has already been populated; decide whether to continue based on tool risk. } else { log.Printf("retryable model feedback: %s", registry.Explain(err)) return } } if report.HasRepairs() { log.Printf("argument repairs: %+v", report.Repairs) } }
By default, toolargs uses MatchingModeToolArgs, which enables deterministic field-name matching:
exact:phoneNumber->phoneNumbernormalized:phone_number->phoneNumberderived_key:phone->phoneNumberwhen it is the only candidate
High-risk tools can require exact schema field names only:
err := toolargs.Register[TicketArgs](registry, toolargs.Tool{ Name: "query_tickets", }, toolargs.WithMatchingMode(toolargs.MatchingModeExact))
Optional fuzzy, semantic, and contextual matching are not enabled by default. Enable them explicitly only when the tool owner can handle UncertainMatchError and log BindReport.
Use WithRequiredAny for generic constraints such as "projectName or projectId must be provided".
err := toolargs.Register[QueryProjectArgs](registry, toolargs.Tool{ Name: "query_projects", Description: "Query projects.", }, toolargs.WithRequiredAny("projectName", "projectId"))
The default schema includes both:
- a natural-language top-level description, for example
must provide one of projectName or projectId - a program-readable
x-required-anyextension
Unknown fields are rejected by default to avoid silently accepting meaningless model output.
Low-risk tools can opt into ignoring unknown fields:
err := toolargs.Register[QueryProjectArgs](registry, toolargs.Tool{ Name: "query_projects", }, toolargs.WithUnknownFieldPolicy(toolargs.UnknownFieldIgnore))
Ignored fields are still recorded in BindReport with the ignored_field layer.
toolargs keeps the core flow stable and exposes extension interfaces:
FieldMatcher: add custom field-name matchingCoercer: add custom type conversionFormatNormalizer: normalize custom field formatsSchemaRenderer: replace schema renderingIssueReporter: replace model-readable error output
Hidden Defaults
Hidden defaults such as includeArchived=false should be injected by your MCP adapter or business assembly layer, not by toolargs.
MIT