run

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Feb 3, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package run is the execution layer for MCP-style tools defined in the model package and resolved via toolindex.

It:

  • Accepts a canonical tool ID (namespace:name) plus arguments
  • Resolves the tool definition and a backend binding
  • Validates inputs and (optionally) outputs against JSON Schema
  • Executes the tool across MCP, provider, and local backends
  • Supports sequential chains with explicit data passing

Scope

run handles execution and chaining only. No discovery, ranking, or documentation. Target MCP protocol version: 2025-11-25 (via model.MCPVersion).

Resolution

Given a tool ID:

  1. Attempt Index.GetTool(id) when Index is configured
  2. If not found, fall back to injected resolvers (ToolResolver, BackendsResolver)

Backend Selection

When multiple backends exist for the same tool, a configurable BackendSelector chooses which to use. The default uses toolindex.DefaultBackendSelector which implements priority: local > provider > mcp.

Validation

Input validation is performed before execution using model.SchemaValidator. Output validation is performed after execution when tool.OutputSchema is present. Both can be configured via ValidateInput and ValidateOutput options.

Chains

Chains execute steps sequentially with explicit data passing. If UsePrevious is true, the prior step's structured result is injected at args["previous"] (overwriting any existing value). Chains stop on first error (v1 policy).

Example

runner := run.NewRunner(
    run.WithIndex(myIndex),
    run.WithMCPExecutor(myMCPExecutor),
)

result, err := runner.Run(ctx, "myns:mytool", map[string]any{"input": "value"})
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Structured)
Example (BasicRun)
package main

import (
	"context"
	"fmt"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/jonwraymond/toolexec/run"
	"github.com/jonwraymond/toolfoundation/model"
)

// simpleLocalRegistry is a basic LocalRegistry implementation for examples.
type simpleLocalRegistry struct {
	handlers map[string]run.LocalHandler
}

func newSimpleLocalRegistry() *simpleLocalRegistry {
	return &simpleLocalRegistry{handlers: make(map[string]run.LocalHandler)}
}

func (r *simpleLocalRegistry) Get(name string) (run.LocalHandler, bool) {
	h, ok := r.handlers[name]
	return h, ok
}

func (r *simpleLocalRegistry) Register(name string, h run.LocalHandler) {
	r.handlers[name] = h
}

func main() {
	// Create a tool
	tool := model.Tool{
		Tool: mcp.Tool{
			Name:        "greet",
			InputSchema: map[string]any{"type": "object"},
		},
	}

	// Create a backend
	backend := model.ToolBackend{
		Kind:  model.BackendKindLocal,
		Local: &model.LocalBackend{Name: "greeter"},
	}

	// Create a local registry with a handler
	localReg := newSimpleLocalRegistry()
	localReg.Register("greeter", func(_ context.Context, args map[string]any) (any, error) {
		name, _ := args["name"].(string)
		if name == "" {
			name = "World"
		}
		return map[string]any{"greeting": "Hello, " + name + "!"}, nil
	})

	// Create resolvers (in production, you'd use toolindex.Index)
	toolResolver := func(id string) (*model.Tool, error) {
		if id == "greet" {
			return &tool, nil
		}
		return nil, fmt.Errorf("tool not found: %s", id)
	}
	backendsResolver := func(id string) ([]model.ToolBackend, error) {
		if id == "greet" {
			return []model.ToolBackend{backend}, nil
		}
		return nil, fmt.Errorf("no backends for: %s", id)
	}

	// Create runner
	runner := run.NewRunner(
		run.WithToolResolver(toolResolver),
		run.WithBackendsResolver(backendsResolver),
		run.WithLocalRegistry(localReg),
		run.WithValidation(false, false), // Disable validation for example
	)

	// Execute the tool
	result, err := runner.Run(context.Background(), "greet", map[string]any{"name": "Claude"})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Access the structured result
	m := result.Structured.(map[string]any)
	fmt.Println(m["greeting"])

}
Output:

Hello, Claude!
Example (ChainExecution)
package main

import (
	"context"
	"fmt"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/jonwraymond/toolexec/run"
	"github.com/jonwraymond/toolfoundation/model"
)

// simpleLocalRegistry is a basic LocalRegistry implementation for examples.
type simpleLocalRegistry struct {
	handlers map[string]run.LocalHandler
}

func newSimpleLocalRegistry() *simpleLocalRegistry {
	return &simpleLocalRegistry{handlers: make(map[string]run.LocalHandler)}
}

func (r *simpleLocalRegistry) Get(name string) (run.LocalHandler, bool) {
	h, ok := r.handlers[name]
	return h, ok
}

func (r *simpleLocalRegistry) Register(name string, h run.LocalHandler) {
	r.handlers[name] = h
}

func main() {
	// Create tools for a processing pipeline
	tools := map[string]model.Tool{
		"fetch": {Tool: mcp.Tool{
			Name:        "fetch",
			InputSchema: map[string]any{"type": "object"},
		}},
		"transform": {Tool: mcp.Tool{
			Name:        "transform",
			InputSchema: map[string]any{"type": "object"},
		}},
		"store": {Tool: mcp.Tool{
			Name:        "store",
			InputSchema: map[string]any{"type": "object"},
		}},
	}

	backends := map[string]model.ToolBackend{}
	for name := range tools {
		backends[name] = model.ToolBackend{
			Kind:  model.BackendKindLocal,
			Local: &model.LocalBackend{Name: name + "-handler"},
		}
	}

	// Create handlers
	localReg := newSimpleLocalRegistry()

	localReg.Register("fetch-handler", func(_ context.Context, _ map[string]any) (any, error) {
		return map[string]any{"data": []string{"item1", "item2", "item3"}}, nil
	})

	localReg.Register("transform-handler", func(_ context.Context, args map[string]any) (any, error) {
		prev, _ := args["previous"].(map[string]any)
		data, _ := prev["data"].([]string)
		transformed := make([]string, len(data))
		for i, item := range data {
			transformed[i] = "processed-" + item
		}
		return map[string]any{"data": transformed}, nil
	})

	localReg.Register("store-handler", func(_ context.Context, args map[string]any) (any, error) {
		prev, _ := args["previous"].(map[string]any)
		data, _ := prev["data"].([]string)
		return map[string]any{
			"stored": len(data),
			"status": "success",
		}, nil
	})

	// Create resolvers
	toolResolver := func(id string) (*model.Tool, error) {
		if t, ok := tools[id]; ok {
			return &t, nil
		}
		return nil, fmt.Errorf("tool not found: %s", id)
	}
	backendsResolver := func(id string) ([]model.ToolBackend, error) {
		if b, ok := backends[id]; ok {
			return []model.ToolBackend{b}, nil
		}
		return nil, fmt.Errorf("no backends for: %s", id)
	}

	// Create runner
	runner := run.NewRunner(
		run.WithToolResolver(toolResolver),
		run.WithBackendsResolver(backendsResolver),
		run.WithLocalRegistry(localReg),
		run.WithValidation(false, false),
	)

	// Define the chain
	steps := []run.ChainStep{
		{ToolID: "fetch"},
		{ToolID: "transform", UsePrevious: true},
		{ToolID: "store", UsePrevious: true},
	}

	// Execute the chain
	final, _, err := runner.RunChain(context.Background(), steps)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	m := final.Structured.(map[string]any)
	fmt.Printf("Stored %v items: %s\n", m["stored"], m["status"])

}
Output:

Stored 3 items: success
Example (CustomBackendSelector)
package main

import (
	"fmt"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/jonwraymond/toolexec/run"
	"github.com/jonwraymond/toolfoundation/model"
)

func main() {
	// Custom selector that prefers provider backends for specific tools
	customSelector := func(backends []model.ToolBackend) model.ToolBackend {
		// First try to find a provider backend
		for _, b := range backends {
			if b.Kind == model.BackendKindProvider {
				return b
			}
		}
		// Fall back to any available backend
		return backends[0]
	}

	// Create a tool with multiple backends
	tool := model.Tool{
		Tool: mcp.Tool{
			Name:        "multi-backend-tool",
			InputSchema: map[string]any{"type": "object"},
		},
	}

	localBackend := model.ToolBackend{
		Kind:  model.BackendKindLocal,
		Local: &model.LocalBackend{Name: "local-handler"},
	}
	providerBackend := model.ToolBackend{
		Kind: model.BackendKindProvider,
		Provider: &model.ProviderBackend{
			ProviderID: "my-provider",
			ToolID:     "provider-tool",
		},
	}

	backends := []model.ToolBackend{localBackend, providerBackend}

	// Create resolvers
	toolResolver := func(_ string) (*model.Tool, error) {
		return &tool, nil
	}
	backendsResolver := func(_ string) ([]model.ToolBackend, error) {
		return backends, nil
	}

	// Create runner with custom selector
	runner := run.NewRunner(
		run.WithToolResolver(toolResolver),
		run.WithBackendsResolver(backendsResolver),
		run.WithBackendSelector(customSelector),
		run.WithValidation(false, false),
	)

	// At this point, the runner would use the provider backend
	// even though a local backend is also available
	_ = runner

	fmt.Println("Runner configured with custom backend selector")

}
Output:

Runner configured with custom backend selector

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrToolNotFound is returned when a tool cannot be resolved.
	ErrToolNotFound = errors.New("tool not found")

	// ErrInvalidToolID is returned when a tool ID is empty or malformed.
	ErrInvalidToolID = errors.New("invalid tool id")

	// ErrNoBackends is returned when a tool has no available backends.
	ErrNoBackends = errors.New("no backends available")

	// ErrValidation is returned when input validation fails.
	ErrValidation = errors.New("validation error")

	// ErrExecution is returned when tool execution fails.
	ErrExecution = errors.New("execution error")

	// ErrOutputValidation is returned when output validation fails.
	ErrOutputValidation = errors.New("output validation error")

	// ErrStreamNotSupported is returned when streaming is not supported
	// by the executor or backend.
	ErrStreamNotSupported = errors.New("streaming not supported")
)

Sentinel errors for common failure conditions.

Functions

func WrapError

func WrapError(toolID string, backend *model.ToolBackend, op string, err error) error

WrapError wraps an error with tool context. Returns nil if err is nil.

Types

type ChainStep

type ChainStep struct {
	// ToolID is the canonical tool identifier to execute.
	ToolID string `json:"toolId"`

	// Args are the arguments to pass to the tool.
	Args map[string]any `json:"args,omitempty"`

	// UsePrevious, when true, injects the previous step's structured result
	// into args["previous"], overwriting any existing value.
	UsePrevious bool `json:"usePrevious,omitempty"`
}

ChainStep defines one step in a sequential chain. Chains execute steps in order, with optional data passing between steps.

type Config

type Config struct {

	// Index is the tool registry for lookup.
	Index index.Index

	// ToolResolver is a fallback function to resolve tools when Index is not
	// configured or returns ErrNotFound.
	ToolResolver func(id string) (*model.Tool, error)

	// BackendsResolver is a fallback function to resolve backends when Index
	// is not configured or returns ErrNotFound.
	BackendsResolver func(id string) ([]model.ToolBackend, error)

	// BackendSelector chooses which backend to use when multiple are available.
	// Defaults to index.DefaultBackendSelector (local > provider > mcp).
	BackendSelector index.BackendSelector

	// Validator validates tool inputs and outputs against JSON Schema.
	// Defaults to model.NewDefaultValidator().
	Validator model.SchemaValidator

	// ValidateInput enables input validation before execution.
	// Defaults to true.
	ValidateInput bool

	// ValidateOutput enables output validation after execution.
	// Defaults to true.
	ValidateOutput bool

	// MCP is the executor for MCP backend tools.
	MCP MCPExecutor

	// Provider is the executor for provider backend tools.
	Provider ProviderExecutor

	// Local is the registry for local handler functions.
	Local LocalRegistry
}

Config controls resolution, validation, and dispatch behavior.

type ConfigOption

type ConfigOption func(*Config)

ConfigOption is a functional option for configuring a Runner.

func WithBackendSelector

func WithBackendSelector(selector index.BackendSelector) ConfigOption

WithBackendSelector sets a custom backend selector function.

func WithBackendsResolver

func WithBackendsResolver(resolver func(id string) ([]model.ToolBackend, error)) ConfigOption

WithBackendsResolver sets a fallback backends resolver function.

func WithIndex

func WithIndex(idx index.Index) ConfigOption

WithIndex sets the tool index for resolution.

func WithLocalRegistry

func WithLocalRegistry(reg LocalRegistry) ConfigOption

WithLocalRegistry sets the local handler registry.

func WithMCPExecutor

func WithMCPExecutor(exec MCPExecutor) ConfigOption

WithMCPExecutor sets the MCP executor.

func WithProviderExecutor

func WithProviderExecutor(exec ProviderExecutor) ConfigOption

WithProviderExecutor sets the provider executor.

func WithToolResolver

func WithToolResolver(resolver func(id string) (*model.Tool, error)) ConfigOption

WithToolResolver sets a fallback tool resolver function.

func WithValidation

func WithValidation(input, output bool) ConfigOption

WithValidation sets whether to validate inputs and outputs.

func WithValidator

func WithValidator(v model.SchemaValidator) ConfigOption

WithValidator sets a custom schema validator.

type DefaultRunner

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

DefaultRunner is the standard Runner implementation. It uses the configured Index, resolvers, validators, and executors to resolve, validate, and execute tools.

func NewRunner

func NewRunner(opts ...ConfigOption) *DefaultRunner

NewRunner creates a new DefaultRunner with the given options. By default, validation is enabled for both input and output.

func (*DefaultRunner) Run

func (r *DefaultRunner) Run(ctx context.Context, toolID string, args map[string]any) (RunResult, error)

Run executes a single tool and returns the normalized result.

func (*DefaultRunner) RunChain

func (r *DefaultRunner) RunChain(ctx context.Context, steps []ChainStep) (RunResult, []StepResult, error)

RunChain executes a sequence of tool steps.

func (*DefaultRunner) RunChainWithProgress

func (r *DefaultRunner) RunChainWithProgress(ctx context.Context, steps []ChainStep, onProgress ProgressCallback) (RunResult, []StepResult, error)

RunChainWithProgress executes a chain and emits progress updates.

func (*DefaultRunner) RunStream

func (r *DefaultRunner) RunStream(ctx context.Context, toolID string, args map[string]any) (<-chan StreamEvent, error)

RunStream executes a tool with streaming support.

func (*DefaultRunner) RunWithProgress

func (r *DefaultRunner) RunWithProgress(ctx context.Context, toolID string, args map[string]any, onProgress ProgressCallback) (RunResult, error)

RunWithProgress executes a tool and emits coarse progress updates.

type LocalHandler

type LocalHandler func(ctx context.Context, args map[string]any) (any, error)

LocalHandler is the function signature for local tool execution. It receives a context and arguments, and returns a result or error. Implementations must be non-nil when returned by LocalRegistry.Get.

type LocalRegistry

type LocalRegistry interface {
	// Get returns the handler for the given name, or false if not found.
	Get(name string) (LocalHandler, bool)
}

LocalRegistry resolves local handlers by name. Implementations provide a mapping from handler names to LocalHandler functions.

Contract: - Concurrency: implementations must be safe for concurrent use. - Ownership: returned handlers must be non-nil when ok is true. - Nil/zero: unknown names must return (nil, false).

type MCPExecutor

type MCPExecutor interface {
	// CallTool executes a tool call and returns the result.
	CallTool(ctx context.Context, serverName string, params *mcp.CallToolParams) (*mcp.CallToolResult, error)

	// CallToolStream executes a tool call with streaming.
	// Implementations may return ErrStreamNotSupported when streaming is unavailable.
	// Contract: if err is nil, the returned channel MUST be non-nil.
	CallToolStream(ctx context.Context, serverName string, params *mcp.CallToolParams) (<-chan StreamEvent, error)
}

MCPExecutor executes MCP tool calls using MCP Go SDK types. Implementations typically wrap an MCP client connection.

Contract: - Concurrency: implementations must be safe for concurrent use. - Context: must honor cancellation/deadlines and return ctx.Err() when canceled. - Errors: return ErrStreamNotSupported for unsupported streaming. - Ownership: params are read-only; returned results/channels are caller-owned. - Nil/zero: serverName and params must be non-empty; invalid inputs should return error.

type ProgressCallback

type ProgressCallback func(ProgressEvent)

ProgressCallback receives progress updates during execution. Implementations should be fast and non-blocking.

type ProgressEvent

type ProgressEvent struct {
	Progress float64 `json:"progress"`
	Total    float64 `json:"total,omitempty"`
	Message  string  `json:"message,omitempty"`
}

ProgressEvent represents coarse-grained progress during execution. Progress and Total are optional; when Total is zero, Progress should be treated as a best-effort signal rather than a precise fraction.

type ProgressRunner

type ProgressRunner interface {
	// RunWithProgress executes a single tool and emits progress updates.
	RunWithProgress(ctx context.Context, toolID string, args map[string]any, onProgress ProgressCallback) (RunResult, error)

	// RunChainWithProgress executes a chain and emits progress updates.
	RunChainWithProgress(ctx context.Context, steps []ChainStep, onProgress ProgressCallback) (RunResult, []StepResult, error)
}

ProgressRunner is an optional interface that provides progress callbacks for long-running tool executions and chains.

Contract: - Concurrency: implementations must be safe for concurrent use. - Context: must honor cancellation/deadlines and return ctx.Err() when canceled. - Progress: callbacks must be invoked in-order; nil callbacks are allowed. - Errors: follow Runner error semantics for underlying execution.

type ProviderExecutor

type ProviderExecutor interface {
	// CallTool executes a provider tool and returns the result.
	CallTool(ctx context.Context, providerID, toolID string, args map[string]any) (any, error)

	// CallToolStream executes a provider tool with streaming.
	// Implementations may return ErrStreamNotSupported when streaming is unavailable.
	// Contract: if err is nil, the returned channel MUST be non-nil.
	CallToolStream(ctx context.Context, providerID, toolID string, args map[string]any) (<-chan StreamEvent, error)
}

ProviderExecutor executes provider-bound tools. It is intentionally generic but uses canonical tool IDs and args.

Contract: - Concurrency: implementations must be safe for concurrent use. - Context: must honor cancellation/deadlines and return ctx.Err() when canceled. - Errors: return ErrStreamNotSupported for unsupported streaming. - Ownership: args are read-only; returned results/channels are caller-owned. - Nil/zero: providerID/toolID must be non-empty; invalid inputs should return error.

type RunResult

type RunResult struct {
	// Tool is the resolved tool definition.
	Tool model.Tool `json:"tool"`

	// Backend is the backend that was used for execution.
	Backend model.ToolBackend `json:"backend"`

	// Structured is the normalized result value.
	// For MCP backends, this is either StructuredContent (preferred)
	// or a best-effort structured value derived from Content.
	// For provider/local backends, this is the executor/handler return value.
	Structured any `json:"structured,omitempty"`

	// MCPResult is the raw MCP CallToolResult when the backend was MCP.
	// Nil for provider and local backends unless they return MCP-native results.
	MCPResult *mcp.CallToolResult `json:"mcpResult,omitempty"`
}

RunResult is the normalized result of a tool execution. Structured is the primary value used for chaining and validation.

type Runner

type Runner interface {
	// Run executes a single tool and returns the normalized result.
	// It resolves the tool, validates input, executes via the appropriate backend,
	// normalizes the result, and validates output.
	Run(ctx context.Context, toolID string, args map[string]any) (RunResult, error)

	// RunStream executes a tool with streaming support.
	// Returns a channel that receives streaming events.
	// May return ErrStreamNotSupported if the backend doesn't support streaming.
	RunStream(ctx context.Context, toolID string, args map[string]any) (<-chan StreamEvent, error)

	// RunChain executes a sequence of tool steps.
	// Returns the final result and a slice of step results.
	// Stops on the first error (v1 policy).
	// If UsePrevious is true for a step, the previous step's Structured result
	// is injected at args["previous"], overwriting any existing value,
	// even when the previous result is nil.
	RunChain(ctx context.Context, steps []ChainStep) (RunResult, []StepResult, error)
}

Runner is the main execution interface for running tools. It provides methods for single tool execution, streaming execution, and sequential chain execution.

Contract:

  • Concurrency: implementations must be safe for concurrent use.
  • Context: must honor cancellation/deadlines and return ctx.Err() when canceled.
  • Errors: failures should be wrapped with ToolError; callers use errors.Is to match ErrInvalidToolID, ErrToolNotFound, ErrValidation, ErrExecution, ErrOutputValidation, and ErrStreamNotSupported.
  • Ownership: args are treated as read-only; results are caller-owned snapshots.
  • Determinism: for identical inputs/backends, results should be stable.
  • Nil/zero: empty toolID must return ErrInvalidToolID; nil args treated as empty.

type StepResult

type StepResult struct {
	// ToolID is the canonical tool identifier that was executed.
	ToolID string `json:"toolId"`

	// Backend is the backend that was used for execution.
	Backend model.ToolBackend `json:"backend"`

	// Result contains the execution result.
	Result RunResult `json:"result"`

	// Err is set if the step failed.
	// Not serialized to JSON - callers should check this field explicitly.
	Err error `json:"-"`
}

StepResult captures what happened at a single chain step. It includes both the result and any error that occurred.

type StreamEvent

type StreamEvent struct {
	// Kind indicates the type of streaming event.
	Kind StreamEventKind `json:"kind"`

	// ToolID is the canonical tool identifier (namespace:name or name).
	ToolID string `json:"toolId,omitempty"`

	// Data contains event-specific payload.
	// For progress events, this might contain percentage or status.
	// For chunk events, this contains partial result data.
	Data any `json:"data,omitempty"`

	// Err is set when Kind is StreamEventError.
	// Not serialized to JSON - callers should extract error information
	// from Data if needed for transmission.
	Err error `json:"-"`
}

StreamEvent is a transport-agnostic streaming envelope. It carries streaming events from tool execution including progress updates, partial chunks, completion signals, and errors.

type StreamEventKind

type StreamEventKind string

StreamEventKind represents streaming and progress events.

const (
	// StreamEventProgress indicates a progress update.
	StreamEventProgress StreamEventKind = "progress"

	// StreamEventChunk indicates a partial result chunk.
	StreamEventChunk StreamEventKind = "chunk"

	// StreamEventDone indicates streaming has completed successfully.
	StreamEventDone StreamEventKind = "done"

	// StreamEventError indicates an error occurred during streaming.
	StreamEventError StreamEventKind = "error"
)

type ToolError

type ToolError struct {
	// ToolID is the canonical tool identifier.
	ToolID string

	// Backend is the backend that was used (may be nil for resolution errors).
	Backend *model.ToolBackend

	// Op is the operation that failed (e.g., "resolve", "validate_input", "execute").
	Op string

	// Err is the underlying error.
	Err error
}

ToolError wraps an error with tool execution context. It preserves the tool ID, backend, and operation for debugging.

func (*ToolError) Error

func (e *ToolError) Error() string

Error returns a formatted error message including context.

func (*ToolError) Is

func (e *ToolError) Is(target error) bool

Is reports whether this error matches the target. It provides special handling to map index.ErrNotFound to ErrToolNotFound.

func (*ToolError) Unwrap

func (e *ToolError) Unwrap() error

Unwrap returns the underlying error for errors.Unwrap.

Jump to

Keyboard shortcuts

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