Go

How to add pre- and post- processing to your Agents using Go.

Prerequisites

This tutorial assumes that you have set up MCP Toolbox with a basic agent as described in the local quickstart.

This guide demonstrates how to implement these patterns in your Toolbox applications.

Implementation

The following example demonstrates how to use the beforeToolCallback and afterToolCallback hooks in the ADK LlmAgent to implement pre and post processing logic.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/googleapis/mcp-toolbox-sdk-go/tbadk"
	"google.golang.org/adk/agent"
	"google.golang.org/adk/agent/llmagent"
	"google.golang.org/adk/model/gemini"
	"google.golang.org/adk/runner"
	"google.golang.org/adk/session"
	"google.golang.org/adk/tool"
	"google.golang.org/genai"
)

const systemPrompt = `You're a helpful hotel assistant. You handle hotel searching, booking and
cancellations. When the user searches for a hotel, mention it's name, id,
location and price tier. Always mention hotel ids while performing any
searches. This is very important for any operations. For any bookings or
cancellations, please provide the appropriate confirmation. Be sure to
update checkin or checkout dates if mentioned by the user.
Don't ask for confirmations from the user.`

var queries = []string{
	"Book hotel with id 3.",
	"Update my hotel with id 3 with checkin date 2025-01-04 and checkout date 2025-01-20",
}

// Pre-processing
func enforceBusinessRules(ctx tool.Context, tool tool.Tool, args map[string]any) (map[string]any, error) {

	fmt.Printf("POLICY CHECK: Intercepting '%s'\n", tool.Name())
	if tool.Name() == "update-hotel" {
		checkinStr, okCheckin := args["checkin_date"].(string)
		checkoutStr, okCheckout := args["checkout_date"].(string)

		if okCheckin && okCheckout {
			startDate, errStart := time.Parse("2006-01-02", checkinStr)
			endDate, errEnd := time.Parse("2006-01-02", checkoutStr)
			if errStart != nil || errEnd != nil {
				return nil, nil
			}

			duration := endDate.Sub(startDate).Hours() / 24
			if duration > 14 {
				fmt.Println("BLOCKED: Stay too long")
				return map[string]any{"Error": "Maximum stay duration is 14 days."}, nil
			}
		}
	}
	return nil, nil
}

// Post-processing
func enrichResponse(ctx tool.Context, tool tool.Tool, args, result map[string]any, err error) (map[string]any, error) {
	resultStr := fmt.Sprintf("%v", result)

	if tool.Name() == "book-hotel" {
		if err != nil {
			return nil, err
		}
		if _, ok := result["Error"]; !ok && !strings.Contains(resultStr, "Error") {
			const loyaltyBonus = 500
			enrichedResult := fmt.Sprintf("Booking Confirmed!\n You earned %d Loyalty Points with this stay.\n\nSystem Details: %s", loyaltyBonus, resultStr)
			return map[string]any{"confirmation": enrichedResult}, nil
		}
	}
	return result, nil
}

func main() {
	genaiKey := os.Getenv("GOOGLE_API_KEY")
	toolboxURL := "http://localhost:5000"
	ctx := context.Background()

	toolboxClient, err := tbadk.NewToolboxClient(toolboxURL)
	if err != nil {
		log.Fatalf("Failed to create MCP Toolbox client: %v", err)
	}

	toolsetName := "my-toolset"
	mcpTools, err := toolboxClient.LoadToolset(toolsetName, ctx)
	if err != nil {
		log.Fatalf("Failed to load MCP toolset '%s': %v\nMake sure your Toolbox server is running.", toolsetName, err)
	}

	model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
		APIKey: genaiKey,
	})
	if err != nil {
		log.Fatalf("Failed to create model: %v", err)
	}

	tools := make([]tool.Tool, len(mcpTools))
	for i := range mcpTools {
		tools[i] = &mcpTools[i]
	}
	llmagent, err := llmagent.New(llmagent.Config{
		Name:        "hotel_assistant",
		Model:       model,
		Description: "Agent to answer questions about hotels.",
		Instruction: systemPrompt,
		Tools:       tools,
		// Add pre- and post- processing hooks
		BeforeToolCallbacks: []llmagent.BeforeToolCallback{enforceBusinessRules},
		AfterToolCallbacks:  []llmagent.AfterToolCallback{enrichResponse},
	})
	if err != nil {
		log.Fatalf("Failed to create agent: %v", err)
	}

	appName := "hotel_assistant"
	userID := "user-123"
	sessionService := session.InMemoryService()
	respSess, err := sessionService.Create(ctx, &session.CreateRequest{
		AppName: appName,
		UserID:  userID,
	})
	if err != nil {
		log.Fatalf("Failed to create the session service: %v", err)
	}
	sess := respSess.Session

	r, err := runner.New(runner.Config{
		AppName:        appName,
		Agent:          llmagent,
		SessionService: sessionService,
	})
	if err != nil {
		log.Fatalf("Failed to create runner: %v", err)
	}

	for i, query := range queries {
		fmt.Printf("\n=== Query %d: %s ===\n", i+1, query)
		userMsg := genai.NewContentFromText(query, genai.RoleUser)
		streamingMode := agent.StreamingModeSSE

		runIter := r.Run(ctx, userID, sess.ID(), userMsg, agent.RunConfig{
			StreamingMode: streamingMode,
		})

		fmt.Print("AI: ")
		for event := range runIter {
			if event != nil && event.Content != nil {
				for _, p := range event.Content.Parts {
					fmt.Print(p.Text)
				}
			}
		}

		fmt.Println("\n" + strings.Repeat("-", 80) + "\n")
	}
}

You can also add model-level (beforeModelCallback, afterModelCallback) and agent-level (beforeAgentCallback, afterAgentCallback) hooks to intercept messages at different stages of the execution loop.

For more information, see the ADK Callbacks documentation.

Results

The output should look similar to the following.

Note

The exact responses may vary due to the non-deterministic nature of LLMs and differences between orchestration frameworks.

AI: Booking Confirmed! You earned 500 Loyalty Points with this stay.

AI: Error: Maximum stay duration is 14 days.