Tools & MCP
Tool registry, function calling, and Model Context Protocol integration.
Overview
Section titled “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
Section titled “Tool Registry”Core Types
Section titled “Core Types”ToolDescriptor
Section titled “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
Section titled “Registry”type Registry struct { repository ToolRepository tools map[string]*ToolDescriptor validator *SchemaValidator executors map[string]Executor}Constructor Functions
Section titled “Constructor Functions”NewRegistry
Section titled “NewRegistry”func NewRegistry() *RegistryCreates registry without repository backend (in-memory only).
Example:
registry := tools.NewRegistry()NewRegistryWithRepository
Section titled “NewRegistryWithRepository”func NewRegistryWithRepository(repository ToolRepository) *RegistryCreates registry with persistent storage backend.
Example:
repo := persistence.NewFileRepository("/tools")registry := tools.NewRegistryWithRepository(repo)Registry Methods
Section titled “Registry Methods”Register
Section titled “Register”func (r *Registry) Register(descriptor *ToolDescriptor) errorRegisters 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)}func (r *Registry) Get(name string) *ToolDescriptorRetrieves tool descriptor by name.
Example:
tool := registry.Get("get_weather")if tool == nil { log.Fatal("Tool not found")}GetToolsByNames
Section titled “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)}func (r *Registry) List() []stringReturns all registered tool names.
Example:
toolNames := registry.List()fmt.Printf("Available tools: %v\n", toolNames)Execute
Section titled “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
Section titled “RegisterExecutor”func (r *Registry) RegisterExecutor(executor Executor)Registers a custom tool executor.
Example:
// Custom executortype 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
Section titled “Tool Executors”Built-in Executors
Section titled “Built-in Executors”MockStaticExecutor:
// Static mock responsestool := &tools.ToolDescriptor{ Name: "get_weather", Mode: "mock", MockResult: json.RawMessage(`{"temp": 72}`),}MockScriptedExecutor:
// Template-based mock responsestool := &tools.ToolDescriptor{ Name: "greet_user", Mode: "mock", MockTemplate: `{"message": "Hello !"}`,}RepositoryExecutor:
// Execute from persistent storageexecutor := tools.NewRepositoryExecutor(repository)registry.RegisterExecutor(executor)MCPExecutor:
// Execute via MCP serversmcpRegistry := mcp.NewRegistry()executor := tools.NewMCPExecutor(mcpRegistry)registry.RegisterExecutor(executor)
tool := &tools.ToolDescriptor{ Name: "read_file", Mode: "mcp", // Routes to MCP executor}HTTP Executor
Section titled “HTTP Executor”// Live HTTP API callstool := &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
Section titled “Tool Validation”Input Validation
Section titled “Input Validation”// Validate arguments against input schemavalidator := tools.NewSchemaValidator()err := validator.ValidateArgs(tool, argsJSON)if err != nil { log.Printf("Invalid arguments: %v", err)}Output Validation
Section titled “Output Validation”// Validate result against output schemaerr := validator.ValidateResult(tool, resultJSON)if err != nil { log.Printf("Invalid result: %v", err)}Tool Policy
Section titled “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)
Section titled “Model Context Protocol (MCP)”Overview
Section titled “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
Section titled “MCP Registry”Core Types
Section titled “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
Section titled “Constructor Functions”NewRegistry:
func NewRegistry() *RegistryImplNewRegistryWithServers:
func NewRegistryWithServers(serverConfigs []ServerConfigData) (*RegistryImpl, error)Example:
registry := mcp.NewRegistry()defer registry.Close()Registry Methods
Section titled “Registry Methods”RegisterServer:
func (r *RegistryImpl) RegisterServer(config ServerConfig) errorRegisters 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
Section titled “MCP Client”Client Interface
Section titled “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
Section titled “Client Creation”// Stdio clientconfig := 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 connectioninfo, 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
Section titled “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 contentfor _, content := range response.Content { if content.Type == "text" { fmt.Println(content.Text) }}MCP Tool Integration
Section titled “MCP Tool Integration”Automatic Discovery
Section titled “Automatic Discovery”// Create MCP registrymcpRegistry := mcp.NewRegistry()defer mcpRegistry.Close()
// Register serversmcpRegistry.RegisterServer(mcp.ServerConfig{ Name: "filesystem", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/allowed"},})
// Create tool registrytoolRegistry := tools.NewRegistry()
// Register MCP executormcpExecutor := tools.NewMCPExecutor(mcpRegistry)toolRegistry.RegisterExecutor(mcpExecutor)
// Discover and register MCP toolsctx := 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
Section titled “Manual Integration”// Define MCP tool manuallytool := &tools.ToolDescriptor{ Name: "read_file", Description: "Read file contents", InputSchema: json.RawMessage(`{ "type": "object", "properties": { "path": {"type": "string"} }, "required": ["path"] }`), Mode: "mcp",}
// Execute via MCPargsJSON := json.RawMessage(`{"path": "/data/file.txt"}`)result, err := toolRegistry.Execute(tool, argsJSON)Examples
Section titled “Examples”Basic Tool Registration
Section titled “Basic Tool Registration”// Create registryregistry := tools.NewRegistry()
// Register mock tooltool := &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)
// Executeargs := json.RawMessage(`{"city": "SF"}`)result, err := registry.Execute(tool, args)MCP Filesystem Integration
Section titled “MCP Filesystem Integration”// Setup MCP registrymcpRegistry := 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 executortoolRegistry := tools.NewRegistry()toolRegistry.RegisterExecutor(tools.NewMCPExecutor(mcpRegistry))
// Discover and register toolsctx := 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 pipelinepipe := 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
Section titled “Custom Tool Executor”// Custom async executor with human approvaltype 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 executorregistry.RegisterExecutor(&ApprovalExecutor{})Best Practices
Section titled “Best Practices”1. Tool Validation
Section titled “1. Tool Validation”// Always validate schemas during registrationerr := registry.Register(tool)if err != nil { log.Printf("Invalid tool schema: %v", err)}2. Error Handling
Section titled “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
Section titled “3. Timeout Configuration”// Set appropriate timeoutstool.TimeoutMs = 5000 // 5 second timeout
// MCP client timeoutoptions := mcp.ClientOptions{ RequestTimeout: 30 * time.Second,}4. Resource Cleanup
Section titled “4. Resource Cleanup”// Always close MCP registriesdefer mcpRegistry.Close()
// Close individual clients if neededdefer client.Close()5. Tool Blocklisting
Section titled “5. Tool Blocklisting”policy := &pipeline.ToolPolicy{ Blocklist: []string{ "delete_database", "system_shutdown", },}Performance Considerations
Section titled “Performance Considerations”Tool Execution Latency
Section titled “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
Section titled “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
Section titled “Optimization Tips”- Cache tool discovery:
// Preload tools at startupserverTools, _ := mcpRegistry.ListAllTools(ctx)- Reuse MCP clients:
// Registry automatically reuses clientsclient, _ := registry.GetClient(ctx, "filesystem")- Parallel tool execution:
// Execute tools concurrently when possiblevar wg sync.WaitGroupfor _, tool := range toolCalls { wg.Add(1) go func(t ToolCall) { defer wg.Done() executeToolAsync(t) }(tool)}wg.Wait()See Also
Section titled “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