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:
- Attempt Index.GetTool(id) when Index is configured
- 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 ¶
- Variables
- func WrapError(toolID string, backend *model.ToolBackend, op string, err error) error
- type ChainStep
- type Config
- type ConfigOption
- func WithBackendSelector(selector index.BackendSelector) ConfigOption
- func WithBackendsResolver(resolver func(id string) ([]model.ToolBackend, error)) ConfigOption
- func WithIndex(idx index.Index) ConfigOption
- func WithLocalRegistry(reg LocalRegistry) ConfigOption
- func WithMCPExecutor(exec MCPExecutor) ConfigOption
- func WithProviderExecutor(exec ProviderExecutor) ConfigOption
- func WithToolResolver(resolver func(id string) (*model.Tool, error)) ConfigOption
- func WithValidation(input, output bool) ConfigOption
- func WithValidator(v model.SchemaValidator) ConfigOption
- type DefaultRunner
- func (r *DefaultRunner) Run(ctx context.Context, toolID string, args map[string]any) (RunResult, error)
- func (r *DefaultRunner) RunChain(ctx context.Context, steps []ChainStep) (RunResult, []StepResult, error)
- func (r *DefaultRunner) RunChainWithProgress(ctx context.Context, steps []ChainStep, onProgress ProgressCallback) (RunResult, []StepResult, error)
- func (r *DefaultRunner) RunStream(ctx context.Context, toolID string, args map[string]any) (<-chan StreamEvent, error)
- func (r *DefaultRunner) RunWithProgress(ctx context.Context, toolID string, args map[string]any, ...) (RunResult, error)
- type LocalHandler
- type LocalRegistry
- type MCPExecutor
- type ProgressCallback
- type ProgressEvent
- type ProgressRunner
- type ProviderExecutor
- type RunResult
- type Runner
- type StepResult
- type StreamEvent
- type StreamEventKind
- type ToolError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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.