Tools & MCP Reference
Tool registry, function calling, and Model Context Protocol integration.
Overview
PromptKit provides comprehensive tool/function calling support through two main systems:
- Tool Registry: Manages tool descriptors, validation, and execution routing
- MCP Integration: Connects to external Model Context Protocol servers for dynamic tools
Both systems work together to provide seamless tool execution in LLM conversations.
Tool Registry
Core Types
ToolDescriptor
type ToolDescriptor struct {
Name string
Description string
InputSchema json.RawMessage // JSON Schema Draft-07
OutputSchema json.RawMessage // JSON Schema Draft-07
Mode string // "mock", "live", "mcp"
TimeoutMs int
MockResult json.RawMessage // Static mock data
MockTemplate string // Template for dynamic mocks
HTTPConfig *HTTPConfig // Live HTTP configuration
}
Registry
type Registry struct {
repository ToolRepository
tools map[string]*ToolDescriptor
validator *SchemaValidator
executors map[string]Executor
}
Constructor Functions
NewRegistry
func NewRegistry() *Registry
Creates registry without repository backend (in-memory only).
Example:
registry := tools.NewRegistry()
NewRegistryWithRepository
func NewRegistryWithRepository(repository ToolRepository) *Registry
Creates registry with persistent storage backend.
Example:
repo := persistence.NewFileRepository("/tools")
registry := tools.NewRegistryWithRepository(repo)
Registry Methods
Register
func (r *Registry) Register(descriptor *ToolDescriptor) error
Registers a tool descriptor with validation.
Example:
tool := &tools.ToolDescriptor{
Name: "get_weather",
Description: "Get current weather for a location",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
}`),
OutputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"temperature": {"type": "number"},
"conditions": {"type": "string"}
}
}`),
Mode: "mock",
MockResult: json.RawMessage(`{"temperature": 72, "conditions": "sunny"}`),
}
if err := registry.Register(tool); err != nil {
log.Fatal(err)
}
Get
func (r *Registry) Get(name string) *ToolDescriptor
Retrieves tool descriptor by name.
Example:
tool := registry.Get("get_weather")
if tool == nil {
log.Fatal("Tool not found")
}
GetToolsByNames
func (r *Registry) GetToolsByNames(names []string) ([]*ToolDescriptor, error)
Retrieves multiple tool descriptors. Returns error if any tool not found.
Example:
tools, err := registry.GetToolsByNames([]string{"get_weather", "search_web"})
if err != nil {
log.Printf("Some tools not found: %v", err)
}
List
func (r *Registry) List() []string
Returns all registered tool names.
Example:
toolNames := registry.List()
fmt.Printf("Available tools: %v\n", toolNames)
Execute
func (r *Registry) Execute(
descriptor *ToolDescriptor,
args json.RawMessage,
) (json.RawMessage, error)
Executes a tool with validated arguments.
Example:
argsJSON := json.RawMessage(`{"location": "San Francisco"}`)
result, err := registry.Execute(tool, argsJSON)
if err != nil {
log.Fatal(err)
}
var weather map[string]interface{}
json.Unmarshal(result, &weather)
fmt.Printf("Temperature: %.0f°F\n", weather["temperature"])
RegisterExecutor
func (r *Registry) RegisterExecutor(executor Executor)
Registers a custom tool executor.
Example:
// Custom executor
type CustomExecutor struct{}
func (e *CustomExecutor) Name() string {
return "custom"
}
func (e *CustomExecutor) Execute(
descriptor *tools.ToolDescriptor,
args json.RawMessage,
) (json.RawMessage, error) {
// Custom execution logic
return json.RawMessage(`{"result": "success"}`), nil
}
registry.RegisterExecutor(&CustomExecutor{})
Tool Executors
Built-in Executors
MockStaticExecutor:
// Static mock responses
tool := &tools.ToolDescriptor{
Name: "get_weather",
Mode: "mock",
MockResult: json.RawMessage(`{"temp": 72}`),
}
MockScriptedExecutor:
// Template-based mock responses
tool := &tools.ToolDescriptor{
Name: "greet_user",
Mode: "mock",
MockTemplate: `{"message": "Hello !"}`,
}
RepositoryExecutor:
// Execute from persistent storage
executor := tools.NewRepositoryExecutor(repository)
registry.RegisterExecutor(executor)
MCPExecutor:
// Execute via MCP servers
mcpRegistry := mcp.NewRegistry()
executor := tools.NewMCPExecutor(mcpRegistry)
registry.RegisterExecutor(executor)
tool := &tools.ToolDescriptor{
Name: "read_file",
Mode: "mcp", // Routes to MCP executor
}
HTTP Executor
// Live HTTP API calls
tool := &tools.ToolDescriptor{
Name: "external_api",
Mode: "live",
HTTPConfig: &tools.HTTPConfig{
URL: "https://api.example.com/endpoint",
Method: "POST",
TimeoutMs: 5000,
Headers: map[string]string{
"Authorization": "Bearer ${API_KEY}",
"Content-Type": "application/json",
},
Redact: []string{"password", "apiKey"},
},
}
Tool Validation
Input Validation
// Validate arguments against input schema
validator := tools.NewSchemaValidator()
err := validator.ValidateArgs(tool, argsJSON)
if err != nil {
log.Printf("Invalid arguments: %v", err)
}
Output Validation
// Validate result against output schema
err := validator.ValidateResult(tool, resultJSON)
if err != nil {
log.Printf("Invalid result: %v", err)
}
Tool Policy
type ToolPolicy struct {
ToolChoice string // "auto", "required", "none", or specific tool
MaxRounds int // Max tool execution rounds
MaxToolCallsPerTurn int // Max tools per LLM response
Blocklist []string // Blocked tool names
}
policy := &pipeline.ToolPolicy{
ToolChoice: "auto",
MaxRounds: 5,
MaxToolCallsPerTurn: 10,
Blocklist: []string{"dangerous_tool"},
}
Model Context Protocol (MCP)
Overview
MCP enables LLMs to interact with external systems through standardized JSON-RPC protocol over stdio.
Supported Transports:
- stdio (currently implemented)
- HTTP/SSE (planned)
Standard MCP Servers:
@modelcontextprotocol/server-filesystem: File operations@modelcontextprotocol/server-memory: Key-value storage- Custom servers: Database, API, system command execution
MCP Registry
Core Types
type RegistryImpl struct {
servers map[string]ServerConfig
clients map[string]Client
toolIndex map[string]string // tool name -> server name
}
type ServerConfig struct {
Name string
Command string
Args []string
Env map[string]string
}
Constructor Functions
NewRegistry:
func NewRegistry() *RegistryImpl
NewRegistryWithServers:
func NewRegistryWithServers(serverConfigs []ServerConfigData) (*RegistryImpl, error)
Example:
registry := mcp.NewRegistry()
defer registry.Close()
Registry Methods
RegisterServer:
func (r *RegistryImpl) RegisterServer(config ServerConfig) error
Registers an MCP server configuration.
Example:
err := registry.RegisterServer(mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/allowed"},
})
if err != nil {
log.Fatal(err)
}
GetClient:
func (r *RegistryImpl) GetClient(
ctx context.Context,
serverName string,
) (Client, error)
Gets or creates a client for the specified server.
Example:
client, err := registry.GetClient(ctx, "filesystem")
if err != nil {
log.Fatal(err)
}
tools, err := client.ListTools(ctx)
if err != nil {
log.Fatal(err)
}
GetClientForTool:
func (r *RegistryImpl) GetClientForTool(
ctx context.Context,
toolName string,
) (Client, error)
Finds the client that provides a specific tool.
Example:
client, err := registry.GetClientForTool(ctx, "read_file")
if err != nil {
log.Fatal(err)
}
ListAllTools:
func (r *RegistryImpl) ListAllTools(
ctx context.Context,
) (map[string][]Tool, error)
Lists all tools from all registered servers.
Example:
serverTools, err := registry.ListAllTools(ctx)
for serverName, tools := range serverTools {
fmt.Printf("Server %s:\n", serverName)
for _, tool := range tools {
fmt.Printf(" - %s: %s\n", tool.Name, tool.Description)
}
}
GetToolSchema:
func (r *RegistryImpl) GetToolSchema(
ctx context.Context,
toolName string,
) (*Tool, error)
Retrieves the schema for a specific tool.
Example:
schema, err := registry.GetToolSchema(ctx, "read_file")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Input schema: %s\n", schema.InputSchema)
MCP Client
Client Interface
type Client interface {
Initialize(ctx context.Context) (*InitializeResponse, error)
ListTools(ctx context.Context) ([]Tool, error)
CallTool(ctx context.Context, name string, arguments json.RawMessage) (*ToolCallResponse, error)
Close() error
IsAlive() bool
}
Client Creation
// Stdio client
config := mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/data"},
Env: map[string]string{"DEBUG": "1"},
}
options := mcp.ClientOptions{
RequestTimeout: 30 * time.Second,
MaxRetries: 3,
RetryBackoff: time.Second,
}
client := mcp.NewStdioClientWithOptions(config, options)
defer client.Close()
// Initialize connection
info, err := client.Initialize(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Server: %s v%s\n", info.ServerInfo.Name, info.ServerInfo.Version)
Tool Operations
List Tools:
tools, err := client.ListTools(ctx)
if err != nil {
log.Fatal(err)
}
for _, tool := range tools {
fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
}
Call Tool:
args := json.RawMessage(`{"path": "/data/file.txt"}`)
response, err := client.CallTool(ctx, "read_file", args)
if err != nil {
log.Fatal(err)
}
// Process response content
for _, content := range response.Content {
if content.Type == "text" {
fmt.Println(content.Text)
}
}
MCP Tool Integration
Automatic Discovery
// Create MCP registry
mcpRegistry := mcp.NewRegistry()
defer mcpRegistry.Close()
// Register servers
mcpRegistry.RegisterServer(mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/allowed"},
})
// Create tool registry
toolRegistry := tools.NewRegistry()
// Register MCP executor
mcpExecutor := tools.NewMCPExecutor(mcpRegistry)
toolRegistry.RegisterExecutor(mcpExecutor)
// Discover and register MCP tools
ctx := context.Background()
serverTools, err := mcpRegistry.ListAllTools(ctx)
if err != nil {
log.Fatal(err)
}
for serverName, mcpTools := range serverTools {
for _, mcpTool := range mcpTools {
// Register as tool descriptor
tool := &tools.ToolDescriptor{
Name: mcpTool.Name,
Description: mcpTool.Description,
InputSchema: mcpTool.InputSchema,
Mode: "mcp", // Routes to MCP executor
}
toolRegistry.Register(tool)
}
}
Manual Integration
// Define MCP tool manually
tool := &tools.ToolDescriptor{
Name: "read_file",
Description: "Read file contents",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}`),
Mode: "mcp",
}
// Execute via MCP
argsJSON := json.RawMessage(`{"path": "/data/file.txt"}`)
result, err := toolRegistry.Execute(tool, argsJSON)
Examples
Basic Tool Registration
// Create registry
registry := tools.NewRegistry()
// Register mock tool
tool := &tools.ToolDescriptor{
Name: "get_temperature",
Description: "Get current temperature",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}`),
OutputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"temperature": {"type": "number"},
"unit": {"type": "string"}
}
}`),
Mode: "mock",
MockResult: json.RawMessage(`{"temperature": 72, "unit": "F"}`),
}
registry.Register(tool)
// Execute
args := json.RawMessage(`{"city": "SF"}`)
result, err := registry.Execute(tool, args)
MCP Filesystem Integration
// Setup MCP registry
mcpRegistry := mcp.NewRegistry()
defer mcpRegistry.Close()
mcpRegistry.RegisterServer(mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/data"},
})
// Setup tool registry with MCP executor
toolRegistry := tools.NewRegistry()
toolRegistry.RegisterExecutor(tools.NewMCPExecutor(mcpRegistry))
// Discover and register tools
ctx := context.Background()
serverTools, _ := mcpRegistry.ListAllTools(ctx)
for _, mcpTools := range serverTools {
for _, mcpTool := range mcpTools {
toolRegistry.Register(&tools.ToolDescriptor{
Name: mcpTool.Name,
Description: mcpTool.Description,
InputSchema: mcpTool.InputSchema,
Mode: "mcp",
})
}
}
// Use in pipeline
pipe := pipeline.NewPipeline(
middleware.ProviderMiddleware(provider, toolRegistry, &pipeline.ToolPolicy{
ToolChoice: "auto",
}, config),
)
result, _ := pipe.Execute(ctx, "user", "Read the contents of data.txt")
Custom Tool Executor
// Custom async executor with human approval
type ApprovalExecutor struct{}
func (e *ApprovalExecutor) Name() string {
return "approval"
}
func (e *ApprovalExecutor) Execute(
descriptor *tools.ToolDescriptor,
args json.RawMessage,
) (json.RawMessage, error) {
// Synchronous fallback
result, err := e.ExecuteAsync(descriptor, args)
if err != nil {
return nil, err
}
if result.Status == tools.ToolStatusPending {
return nil, fmt.Errorf("tool requires approval")
}
return result.Content, nil
}
func (e *ApprovalExecutor) ExecuteAsync(
descriptor *tools.ToolDescriptor,
args json.RawMessage,
) (*tools.ToolExecutionResult, error) {
// Check if approval required
if requiresApproval(descriptor, args) {
return &tools.ToolExecutionResult{
Status: tools.ToolStatusPending,
PendingInfo: &tools.PendingToolInfo{
Reason: "requires_approval",
Message: "Manager approval required",
ToolName: descriptor.Name,
Args: args,
},
}, nil
}
// Execute immediately
result := executeAction(descriptor, args)
return &tools.ToolExecutionResult{
Status: tools.ToolStatusComplete,
Content: result,
}, nil
}
// Register executor
registry.RegisterExecutor(&ApprovalExecutor{})
Best Practices
1. Tool Validation
// Always validate schemas during registration
err := registry.Register(tool)
if err != nil {
log.Printf("Invalid tool schema: %v", err)
}
2. Error Handling
result, err := registry.Execute(tool, args)
if err != nil {
// Check for validation errors
if validErr, ok := err.(*tools.ValidationError); ok {
log.Printf("Validation failed at %s: %s", validErr.Path, validErr.Detail)
}
return err
}
3. Timeout Configuration
// Set appropriate timeouts
tool.TimeoutMs = 5000 // 5 second timeout
// MCP client timeout
options := mcp.ClientOptions{
RequestTimeout: 30 * time.Second,
}
4. Resource Cleanup
// Always close MCP registries
defer mcpRegistry.Close()
// Close individual clients if needed
defer client.Close()
5. Tool Blocklisting
policy := &pipeline.ToolPolicy{
Blocklist: []string{
"delete_database",
"system_shutdown",
},
}
Performance Considerations
Tool Execution Latency
- Mock tools: <1ms
- Repository tools: 1-5ms
- HTTP tools: 100-1000ms (network dependent)
- MCP tools: 10-100ms (process spawn + IPC)
MCP Overhead
- Server startup: 100-500ms (first call only)
- Tool discovery: 50-200ms (cached after first call)
- Tool execution: 10-50ms base overhead + tool execution time
Optimization Tips
- Cache tool discovery:
// Preload tools at startup
serverTools, _ := mcpRegistry.ListAllTools(ctx)
- Reuse MCP clients:
// Registry automatically reuses clients
client, _ := registry.GetClient(ctx, "filesystem")
- Parallel tool execution:
// Execute tools concurrently when possible
var wg sync.WaitGroup
for _, tool := range toolCalls {
wg.Add(1)
go func(t ToolCall) {
defer wg.Done()
executeToolAsync(t)
}(tool)
}
wg.Wait()
See Also
- Pipeline Reference - Using tools in pipelines
- Tools How-To - Tool implementation guide
- MCP Tutorial - Step-by-step MCP setup
- Tools Explanation - Tool system architecture