Documentation
¶
Overview ¶
Package handler provides generic, typed execution wrappers for LLM tool calls.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidArguments = errors.New("invalid arguments")
ErrInvalidArguments is returned by Execute when the raw JSON arguments cannot be unmarshalled into the tool's typed input struct.
This sentinel lets the observable layer (observable.ExecuteRx) stop retrying immediately — retrying the same malformed bytes will always produce the same unmarshal error, so consuming the retry budget is wasteful.
Use errors.Is(err, handler.ErrInvalidArguments) to detect this case.
Functions ¶
This section is empty.
Types ¶
type ExecutableTool ¶
type ExecutableTool interface {
model.Tool
// Execute unmarshals rawArgs into the tool's typed In struct, calls the
// handler, and returns (result, error).
Execute(ctx context.Context, rawArgs json.RawMessage) (any, error)
}
ExecutableTool extends model.Tool with an Execute method.
The agent dispatch loop type-asserts to ExecutableTool after looking up a tool by name in the Registry:
tool, ok := reg.ByName(call.Function.Name) exec, ok := tool.(handler.ExecutableTool) result, err := exec.Execute(ctx, call.Function.Arguments)
Use NewTool to construct an ExecutableTool from a typed ToolHandler.
func NewTool ¶
func NewTool[In any, Out any](name, description string, handler ToolHandler[In, Out]) ExecutableTool
NewTool creates an ExecutableTool from a name, description, and typed handler.
The JSON Schema is derived once at construction time from In's struct tags (json, desc, enum) via model.NewInputSchemaFromStruct. Strict mode is always enabled. Panics if In is not a struct (or *struct).
Example:
type SearchArgs struct {
Query string `json:"query" desc:"The search query."`
}
tool := handler.NewTool("search_web", "Search the web.",
func(ctx context.Context, in SearchArgs) ([]Result, error) {
return repo.Search(ctx, in.Query)
},
)
Example ¶
package main
import (
"context"
"fmt"
"github.com/v8tix/mcp-toolkit/handler"
)
type searchArgs struct {
Query string `json:"query" desc:"Search query."`
Limit *int `json:"limit,omitempty" desc:"Max results."`
}
type searchResult struct {
URL string `json:"url"`
Title string `json:"title"`
}
func main() {
tool := handler.NewTool("search_web", "Search the web.",
func(_ context.Context, in searchArgs) ([]searchResult, error) {
return []searchResult{{URL: "https://example.com", Title: in.Query}}, nil
},
)
def := tool.Definition()
fmt.Println(def.Function.Name)
fmt.Println(def.Function.Parameters.Properties["query"].Description)
}
Output: search_web Search query.
func NewToolWithDefinition ¶
func NewToolWithDefinition[In any, Out any](def model.ToolDefinition, handler ToolHandler[In, Out]) ExecutableTool
NewToolWithDefinition creates an ExecutableTool using a caller-supplied ToolDefinition instead of deriving one from In's struct tags. Use this when you need non-strict mode, custom descriptions, or a schema that cannot be expressed via struct tags alone.
def := schema.FormatToolDefinition("search_web", "Search.", params, false)
tool := handler.NewToolWithDefinition(def, func(ctx context.Context, in SearchArgs) ([]Result, error) {
return repo.Search(ctx, in.Query)
})
Example ¶
package main
import (
"context"
"fmt"
"github.com/v8tix/mcp-toolkit/handler"
"github.com/v8tix/mcp-toolkit/schema"
)
type searchArgs struct {
Query string `json:"query" desc:"Search query."`
Limit *int `json:"limit,omitempty" desc:"Max results."`
}
type searchResult struct {
URL string `json:"url"`
Title string `json:"title"`
}
func main() {
params := schema.NewInputSchemaFromStruct(searchArgs{})
def := schema.FormatToolDefinition("search_web", "Search the web.", params, false)
tool := handler.NewToolWithDefinition(def,
func(_ context.Context, in searchArgs) ([]searchResult, error) {
return []searchResult{{URL: "https://example.com", Title: in.Query}}, nil
},
)
fmt.Println(tool.Definition().Function.Name)
fmt.Println(tool.Definition().Function.Strict)
}
Output: search_web false
func Wrap ¶
func Wrap(inner ExecutableTool, middleware ToolMiddleware) ExecutableTool
Wrap applies a ToolMiddleware around an ExecutableTool's Execute method. The inner tool's Definition() is forwarded unchanged.
Multiple decorators can be stacked by wrapping the result of one Wrap call with another. The last Wrap call is outermost (executes first).
Example:
tool := handler.NewTool("search_web", "...", myHandler)
tool = handler.Wrap(tool, withTimeout(5*time.Second))
tool = handler.Wrap(tool, withLogging(log.Printf))
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/v8tix/mcp-toolkit/handler"
)
type searchArgs struct {
Query string `json:"query" desc:"Search query."`
Limit *int `json:"limit,omitempty" desc:"Max results."`
}
type searchResult struct {
URL string `json:"url"`
Title string `json:"title"`
}
func main() {
base := handler.NewTool("search_web", "Search the web.",
func(_ context.Context, in searchArgs) ([]searchResult, error) {
return nil, nil
},
)
logged := handler.Wrap(base, func(ctx context.Context, rawArgs json.RawMessage, next handler.ExecuteFunc) (any, error) {
log.Printf("calling search_web with %s", rawArgs)
return next(ctx, rawArgs)
})
fmt.Println(logged.Definition().Function.Name)
}
Output: search_web
type ExecuteFunc ¶
ExecuteFunc is the signature of the next hop in a middleware chain.
type ToolHandler ¶
ToolHandler is the typed execution function every executable tool implements.
In is the args struct populated by JSON-unmarshalling the LLM's tool-call arguments. Its fields map 1:1 to the tool's JSON Schema properties — the same struct drives both the schema (via NewTool) and the function signature.
Out is the result type returned to the agent loop.
type ToolMiddleware ¶
type ToolMiddleware func(ctx context.Context, rawArgs json.RawMessage, next ExecuteFunc) (any, error)
ToolMiddleware is a function that wraps an Execute call. Middleware can add logging, retry, timeout, tracing, etc. without modifying the underlying tool.
Example — a simple logger middleware:
func WithLogging(log func(string, ...any)) handler.ToolMiddleware {
return func(ctx context.Context, rawArgs json.RawMessage, next handler.ExecuteFunc) (any, error) {
log("tool call", "args", string(rawArgs))
result, err := next(ctx, rawArgs)
if err != nil { log("tool error", "error", err) }
return result, err
}
}