handler

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 29, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package handler provides generic, typed execution wrappers for LLM tool calls.

Index

Examples

Constants

This section is empty.

Variables

View Source
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.Name)
exec, ok := tool.(handler.ExecutableTool)
result, err := exec.Execute(ctx, call.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, description, enum) via schema.StrictTool. Strict mode is always enabled. Panics if In is not a struct (or *struct).

Example:

type SearchArgs struct {
    Query string `json:"query" description:"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/v2/handler"
)

type searchArgs struct {
	Query string `json:"query" description:"Search query."`
	Limit *int   `json:"limit,omitempty" description:"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.Name)
	props := def.InputSchema.(map[string]any)["properties"].(map[string]any)
	fmt.Println(props["query"].(map[string]any)["description"])
}
Output:
search_web
Search query.

func NewToolWithDefinition

func NewToolWithDefinition[In any, Out any](def *sdkmcp.Tool, handler ToolHandler[In, Out]) ExecutableTool

NewToolWithDefinition creates an ExecutableTool using a caller-supplied *sdkmcp.Tool 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.Tool("search_web", "Search.", SearchArgs{})
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/v2/handler"
	"github.com/v8tix/mcp-toolkit/v2/schema"
)

type searchArgs struct {
	Query string `json:"query" description:"Search query."`
	Limit *int   `json:"limit,omitempty" description:"Max results."`
}

type searchResult struct {
	URL   string `json:"url"`
	Title string `json:"title"`
}

func main() {
	def := schema.Tool("search_web", "Search the web.", searchArgs{})

	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().Name)
	s := tool.Definition().InputSchema.(map[string]any)
	_, hasAdditional := s["additionalProperties"]
	fmt.Println(hasAdditional)
}
Output:
search_web
false

type ExecuteFunc

type ExecuteFunc func(ctx context.Context, rawArgs json.RawMessage) (any, error)

ExecuteFunc is the signature of the next hop in a middleware chain.

type ToolHandler

type ToolHandler[In any, Out any] func(ctx context.Context, in In) (Out, error)

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
    }
}

type WrappedTool

type WrappedTool struct {
	// contains filtered or unexported fields
}

WrappedTool is the Decorator produced by Wrap. Exported so callers can chain additional middleware via its Wrap method without a type assertion.

func Wrap

func Wrap(inner ExecutableTool, middleware ToolMiddleware) *WrappedTool

Wrap applies a ToolMiddleware around an ExecutableTool's Execute method. The inner tool's Definition() is forwarded unchanged. Returns *WrappedTool so additional middleware can be chained via .Wrap().

tool := handler.Wrap(myTool, withTimeout(5*time.Second)).
    Wrap(withLogging(log.Printf)).
    Wrap(withMetrics(meter))
Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/v8tix/mcp-toolkit/v2/handler"
)

type searchArgs struct {
	Query string `json:"query" description:"Search query."`
	Limit *int   `json:"limit,omitempty" description:"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().Name)
}
Output:
search_web

func (*WrappedTool) Definition

func (w *WrappedTool) Definition() *sdkmcp.Tool

func (*WrappedTool) Execute

func (w *WrappedTool) Execute(ctx context.Context, rawArgs json.RawMessage) (any, error)

func (*WrappedTool) Wrap

func (w *WrappedTool) Wrap(middleware ToolMiddleware) *WrappedTool

Wrap applies another ToolMiddleware on top of this one, enabling fluent chaining. The new middleware executes before (outside) the receiver.

tool := handler.Wrap(myTool, withTimeout(5*time.Second)).
    Wrap(withLogging(log.Printf))

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL