Skip to content

Implementing Tools

Tools provide functionality that LLMs can invoke to take actions or perform computations. Think of them as function calls that extend the LLM's capabilities.

Tool Fundamentals

Tools are the primary way LLMs interact with your server to perform actions. They have structured schemas that define parameters, types, and constraints, ensuring type-safe interactions.

Basic Tool Structure

// Create a simple tool
tool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform arithmetic operations"),
    mcp.WithString("operation", 
        mcp.Required(),
        mcp.Enum("add", "subtract", "multiply", "divide"),
        mcp.Description("The arithmetic operation to perform"),
    ),
    mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
    mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
)

Tool Definition

Parameter Types

MCP-Go supports various parameter types with validation:

// String parameters
mcp.WithString("name", 
    mcp.Required(),
    mcp.Description("User's name"),
    mcp.MinLength(1),
    mcp.MaxLength(100),
)
 
// Number parameters  
mcp.WithNumber("age",
    mcp.Required(),
    mcp.Description("User's age"),
    mcp.Minimum(0),
    mcp.Maximum(150),
)
 
// Integer parameters
mcp.WithInteger("count",
    mcp.Default(10),
    mcp.Description("Number of items"),
    mcp.Minimum(1),
    mcp.Maximum(1000),
)
 
// Boolean parameters
mcp.WithBoolean("enabled",
    mcp.Default(true),
    mcp.Description("Whether feature is enabled"),
)
 
// Array parameters
mcp.WithArray("tags",
    mcp.Description("List of tags"),
    mcp.Items(map[string]any{"type": "string"}),
)
 
// Object parameters
mcp.WithObject("config",
    mcp.Description("Configuration object"),
    mcp.Properties(map[string]any{
        "timeout": map[string]any{"type": "number"},
        "retries": map[string]any{"type": "integer"},
    }),
)

Enums and Constraints

// Enum values
mcp.WithString("priority",
    mcp.Required(),
    mcp.Enum("low", "medium", "high", "critical"),
    mcp.Description("Task priority level"),
)
 
// String constraints
mcp.WithString("email",
    mcp.Required(),
    mcp.Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

  
    
    
    Implementing Tools – MCP-Go
    
    
  
  
    
), mcp.Description("Valid email address"), ) // Number constraints mcp.WithNumber("price", mcp.Required(), mcp.Minimum(0), mcp.ExclusiveMaximum(10000), mcp.Description("Product price in USD"), )

Tool Handlers

Tool handlers process the actual function calls from LLMs. MCP-Go provides convenient helper methods for safe parameter extraction.

Parameter Extraction Methods

MCP-Go offers several helper methods on CallToolRequest for type-safe parameter access:

// Required parameters - return error if missing or wrong type
name, err := req.RequireString("name")
age, err := req.RequireInt("age") 
price, err := req.RequireFloat("price")
enabled, err := req.RequireBool("enabled")
 
// Optional parameters with defaults
name := req.GetString("name", "default")
count := req.GetInt("count", 10)
price := req.GetFloat("price", 0.0)
enabled := req.GetBool("enabled", false)
 
// Structured data binding
type Config struct {
    Timeout int    `json:"timeout"`
    Retries int    `json:"retries"`
    Debug   bool   `json:"debug"`
}
var config Config
if err := req.BindArguments(&config); err != nil {
    return mcp.NewToolResultError(err.Error()), nil
}
 
// Raw access (for backward compatibility)
args := req.GetArguments() // returns map[string]any
rawArgs := req.GetRawArguments() // returns any

Basic Handler Pattern

func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Extract parameters using helper methods
    operation, err := req.RequireString("operation")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    x, err := req.RequireFloat("x")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    y, err := req.RequireFloat("y")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // Perform calculation
    var result float64
    switch operation {
    case "add":
        result = x + y
    case "subtract":
        result = x - y
    case "multiply":
        result = x * y
    case "divide":
        if y == 0 {
            return mcp.NewToolResultError("division by zero"), nil
        }
        result = x / y
    default:
        return mcp.NewToolResultError(fmt.Sprintf("unknown operation: %s", operation)), nil
    }
    
    // Return result
    return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil
}

File Operations Tool

func main() {
    s := server.NewMCPServer("File Tools", "1.0.0",
        server.WithToolCapabilities(true),
    )
 
    // File creation tool
    createFileTool := mcp.NewTool("create_file",
        mcp.WithDescription("Create a new file with content"),
        mcp.WithString("path", 
            mcp.Required(),
            mcp.Description("File path to create"),
        ),
        mcp.WithString("content",
            mcp.Required(), 
            mcp.Description("File content"),
        ),
        mcp.WithString("encoding",
            mcp.Default("utf-8"),
            mcp.Enum("utf-8", "ascii", "base64"),
            mcp.Description("File encoding"),
        ),
    )
 
    s.AddTool(createFileTool, handleCreateFile)
    server.ServeStdio(s)
}
 
func handleCreateFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    path, err := req.RequireString("path")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    content, err := req.RequireString("content")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    encoding := req.GetString("encoding", "utf-8")
    
    // Validate path for security
    if strings.Contains(path, "..") {
        return mcp.NewToolResultError("invalid path: directory traversal not allowed"), nil
    }
    
    // Handle different encodings
    var data []byte
    switch encoding {
    case "utf-8":
        data = []byte(content)
    case "ascii":
        data = []byte(content)
    case "base64":
        var err error
        data, err = base64.StdEncoding.DecodeString(content)
        if err != nil {
            return mcp.NewToolResultError(fmt.Sprintf("invalid base64 content: %v", err)), nil
        }
    }
    
    // Create file
    if err := os.WriteFile(path, data, 0644); err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to create file: %v", err)), nil
    }
    
    return mcp.NewToolResultText(fmt.Sprintf("File created successfully: %s", path)), nil
}

Database Query Tool

func handleDatabaseQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Define struct to bind both Query and Params
    var args struct {
        Query  string        `json:"query"`
        Params []interface{} `json:"params"`
    }
    
    // Bind arguments to the struct
    if err := req.BindArguments(&args); err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // Extract values from the bound struct
    query := args.Query
    params := args.Params
    
    // Validate query for security (basic example)
    if !isSelectQuery(query) {
        return mcp.NewToolResultError("only SELECT queries are allowed"), nil
    }
    
    // Execute query with timeout
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    rows, err := db.QueryContext(ctx, query, params...)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("query failed: %v", err)), nil
    }
    defer rows.Close()
    
    // Convert results to JSON
    results, err := rowsToJSON(rows)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to process results: %v", err)), nil
    }
    
    resultData := map[string]interface{}{
        "query":   query,
        "results": results,
        "count":   len(results),
    }
    
    jsonData, err := json.Marshal(resultData)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal results: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}
 
func isSelectQuery(query string) bool {
    trimmed := strings.TrimSpace(strings.ToUpper(query))
    return strings.HasPrefix(trimmed, "SELECT")
}

HTTP Request Tool

func handleHTTPRequest(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    url, err := req.RequireString("url")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    method, err := req.RequireString("method")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    body := req.GetString("body", "")
    
    // Handle headers (optional object parameter)
    var headers map[string]interface{}
    if args := req.GetArguments(); args != nil {
        if h, ok := args["headers"].(map[string]interface{}); ok {
            headers = h
        }
    }
    
    // Create HTTP request
    httpReq, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(body))
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil
    }
    
    // Add headers
    for key, value := range headers {
        httpReq.Header.Set(key, fmt.Sprintf("%v", value))
    }
    
    // Execute request with timeout
    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(httpReq)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
    }
    defer resp.Body.Close()
    
    // Read response
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil
    }
    
    resultData := map[string]interface{}{
        "status_code": resp.StatusCode,
        "headers":     resp.Header,
        "body":        string(respBody),
    }
    
    jsonData, err := json.Marshal(resultData)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Argument Validation

Type-Safe Parameter Extraction

MCP-Go provides helper methods for safe parameter extraction:

func handleValidatedTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Required parameters with validation
    name, err := req.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    age, err := req.RequireFloat("age")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // Optional parameter with default
    enabled := req.GetBool("enabled", true)
    
    // Validate constraints
    if len(name) == 0 {
        return mcp.NewToolResultError("name cannot be empty"), nil
    }
    
    if age < 0 || age > 150 {
        return mcp.NewToolResultError("age must be between 0 and 150"), nil
    }
    
    // Process with validated parameters
    result := processUser(name, int(age), enabled)
    
    jsonData, err := json.Marshal(result)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Available Helper Methods

// Required parameters (return error if missing or wrong type)
name, err := req.RequireString("name")
age, err := req.RequireInt("age")
price, err := req.RequireFloat("price")
enabled, err := req.RequireBool("enabled")
 
// Optional parameters with defaults
name := req.GetString("name", "default")
count := req.GetInt("count", 10)
price := req.GetFloat("price", 0.0)
enabled := req.GetBool("enabled", false)
 
// Structured data binding
type UserData struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
var user UserData
if err := req.BindArguments(&user); err != nil {
    return mcp.NewToolResultError(err.Error()), nil
}
 
### Custom Validation Functions
 
```go
func validateEmail(email string) error {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

  
    
    
    Implementing Tools – MCP-Go
    
    
  
  
    
) if !emailRegex.MatchString(email) { return fmt.Errorf("invalid email format") } return nil } func validateURL(url string) error { parsed, err := url.Parse(url) if err != nil { return fmt.Errorf("invalid URL format: %w", err) } if parsed.Scheme != "http" && parsed.Scheme != "https" { return fmt.Errorf("URL must use http or https scheme") } return nil }

Result Types

Text Results

func handleTextTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    message := "Operation completed successfully"
    return mcp.NewToolResultText(message), nil
}

JSON Results

func handleJSONTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    result := map[string]interface{}{
        "status":    "success",
        "timestamp": time.Now().Unix(),
        "data": map[string]interface{}{
            "processed": 42,
            "errors":    0,
        },
    }
    
    jsonData, err := json.Marshal(result)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Multiple Content Types

func handleMultiContentTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    data := map[string]interface{}{
        "name": "John Doe",
        "age":  30,
    }
    
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            {
                Type: "text",
                Text: "User information retrieved successfully",
            },
            {
                Type: "text",
                Text: fmt.Sprintf("Name: %s, Age: %d", data["name"], data["age"]),
            },
        },
    }, nil
}

Error Results

func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // For validation errors, return error result (not Go error)
    name, err := req.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // For business logic errors, also return error result
    if someCondition {
        return mcp.NewToolResultError("invalid input: " + reason), nil
    }
    
    // For system errors, you can return Go errors
    if systemError {
        return nil, fmt.Errorf("system failure: %v", err)
    }
    
    // Or return structured error information
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            {
                Type: "text", 
                Text: "Operation failed",
            },
        },
        IsError: true,
    }, nil
}

Tool Annotations

Provide hints to help LLMs use your tools effectively:

tool := mcp.NewTool("search_database",
    mcp.WithDescription("Search the product database"),
    mcp.WithString("query",
        mcp.Required(),
        mcp.Description("Search query (supports wildcards with *)"),
    ),
    mcp.WithNumber("limit",
        mcp.DefaultNumber(10),
        mcp.Minimum(1),
        mcp.Maximum(100),
        mcp.Description("Maximum number of results to return"),
    ),
    mcp.WithArray("categories",
        mcp.Description("Filter by product categories"),
        mcp.Items(map[string]any{"type": "string"}),
    ),
)
 
s.AddTool(tool, handleSearchDatabase)

Advanced Tool Patterns

Streaming Results

For long-running operations, consider streaming results:

func handleStreamingTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // For operations that take time, provide progress updates
    results := []string{}
    
    for i := 0; i < 10; i++ {
        // Simulate work
        time.Sleep(100 * time.Millisecond)
        
        // Check for cancellation
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
        
        results = append(results, fmt.Sprintf("Processed item %d", i+1))
    }
    
    resultData := map[string]interface{}{
        "status":  "completed",
        "results": results,
    }
    
    jsonData, err := json.Marshal(resultData)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Conditional Tools

Tools that are only available under certain conditions:

func addConditionalTools(s *server.MCPServer, userRole string) {
    // Admin-only tools
    if userRole == "admin" {
        adminTool := mcp.NewTool("delete_user",
            mcp.WithDescription("Delete a user account (admin only)"),
            mcp.WithString("user_id", mcp.Required()),
        )
        s.AddTool(adminTool, handleDeleteUser)
    }
    
    // User tools available to all
    userTool := mcp.NewTool("get_profile",
        mcp.WithDescription("Get user profile information"),
    )
    s.AddTool(userTool, handleGetProfile)
}

Next Steps

  • Prompts - Learn to create reusable interaction templates
  • Advanced Features - Explore typed tools, middleware, and hooks
  • Resources - Learn about exposing data sources