Skip to content

Tool Hosting

Tool Hosting allows you to register custom tools directly within your Go application. These tools run in-process with zero network overhead, making them ideal for:

  • Application-specific business logic
  • High-performance operations
  • Testing and development
  • Tools that need access to application state

DeepIntShield automatically creates an internal MCP server (bifrostInternal) when you register your first tool.


Create a schema that describes your tool’s parameters:

import "github.com/maximhq/deepintshield/core/schemas"
// Define the tool schema
calculatorSchema := schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "calculator",
Description: schemas.Ptr("Perform basic arithmetic operations"),
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: &schemas.OrderedMap{
"operation": map[string]interface{}{
"type": "string",
"description": "The arithmetic operation to perform",
"enum": []string{"add", "subtract", "multiply", "divide"},
},
"a": map[string]interface{}{
"type": "number",
"description": "First operand",
},
"b": map[string]interface{}{
"type": "number",
"description": "Second operand",
},
},
Required: []string{"operation", "a", "b"},
},
},
}

Create a function that handles tool execution:

func calculatorHandler(args any) (string, error) {
// Parse arguments
argsMap, ok := args.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid arguments")
}
operation, _ := argsMap["operation"].(string)
a, _ := argsMap["a"].(float64)
b, _ := argsMap["b"].(float64)
var result float64
switch operation {
case "add":
result = a + b
case "subtract":
result = a - b
case "multiply":
result = a * b
case "divide":
if b == 0 {
return "", fmt.Errorf("division by zero")
}
result = a / b
default:
return "", fmt.Errorf("unknown operation: %s", operation)
}
return fmt.Sprintf("%.2f", result), nil
}

Register your tool with DeepIntShield:

import (
"context"
deepintshield "github.com/maximhq/deepintshield/core"
"github.com/maximhq/deepintshield/core/schemas"
)
func main() {
// Initialize DeepIntShield with MCP enabled (even empty config is fine)
client, err := deepintshield.Init(context.Background(), schemas.DeepIntShieldConfig{
Account: account,
MCPConfig: &schemas.MCPConfig{}, // Required for tool registration
})
if err != nil {
panic(err)
}
// Register the calculator tool
err = client.RegisterMCPTool(
"calculator",
"Perform basic arithmetic operations",
calculatorHandler,
calculatorSchema,
)
if err != nil {
panic(fmt.Sprintf("Failed to register tool: %v", err))
}
// Now the tool is available in all chat requests
}

Here’s a complete example with multiple tools:

package main
import (
"context"
"encoding/json"
"fmt"
"time"
deepintshield "github.com/maximhq/deepintshield/core"
"github.com/maximhq/deepintshield/core/schemas"
)
func main() {
// Initialize with empty MCP config to enable tool registration
client, err := deepintshield.Init(context.Background(), schemas.DeepIntShieldConfig{
Account: schemas.Account{
Provider: schemas.OpenAI,
APIKey: "your-api-key",
},
MCPConfig: &schemas.MCPConfig{},
})
if err != nil {
panic(err)
}
// Register a calculator tool
registerCalculator(client)
// Register a time tool
registerTimeTool(client)
// Make a request - tools are automatically available
response, err := client.ChatCompletionRequest(schemas.NewDeepIntShieldContext(context.Background(), schemas.NoDeadline), &schemas.DeepIntShieldChatRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: schemas.ChatMessageContent{
ContentStr: deepintshield.Ptr("What is 15 * 7? Also, what time is it?"),
},
},
},
})
if err != nil {
panic(err)
}
// Handle tool calls...
}
func registerCalculator(client *deepintshield.DeepIntShield) {
schema := schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "calculator",
Description: schemas.Ptr("Perform arithmetic: add, subtract, multiply, divide"),
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: &schemas.OrderedMap{
"operation": map[string]interface{}{
"type": "string",
"enum": []string{"add", "subtract", "multiply", "divide"},
},
"a": map[string]interface{}{"type": "number"},
"b": map[string]interface{}{"type": "number"},
},
Required: []string{"operation", "a", "b"},
},
},
}
handler := func(args any) (string, error) {
m := args.(map[string]interface{})
op := m["operation"].(string)
a := m["a"].(float64)
b := m["b"].(float64)
var result float64
switch op {
case "add":
result = a + b
case "subtract":
result = a - b
case "multiply":
result = a * b
case "divide":
if b == 0 {
return "", fmt.Errorf("cannot divide by zero")
}
result = a / b
}
return fmt.Sprintf("%.2f", result), nil
}
if err := client.RegisterMCPTool("calculator", "Arithmetic calculator", handler, schema); err != nil {
panic(err)
}
}
func registerTimeTool(client *deepintshield.DeepIntShield) {
schema := schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "get_current_time",
Description: schemas.Ptr("Get the current date and time"),
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: &schemas.OrderedMap{
"timezone": map[string]interface{}{
"type": "string",
"description": "Timezone (e.g., 'America/New_York', 'UTC')",
},
},
Required: []string{},
},
},
}
handler := func(args any) (string, error) {
m := args.(map[string]interface{})
tzName, _ := m["timezone"].(string)
var loc *time.Location
var err error
if tzName != "" {
loc, err = time.LoadLocation(tzName)
if err != nil {
return "", fmt.Errorf("invalid timezone: %s", tzName)
}
} else {
loc = time.UTC
}
now := time.Now().In(loc)
return now.Format("2006-01-02 15:04:05 MST"), nil
}
if err := client.RegisterMCPTool("get_current_time", "Get current time", handler, schema); err != nil {
panic(err)
}
}

For better type safety, use typed structs with JSON marshaling:

// Define typed arguments
type WeatherArgs struct {
City string `json:"city"`
Units string `json:"units,omitempty"` // celsius or fahrenheit
}
type WeatherResponse struct {
City string `json:"city"`
Temperature float64 `json:"temperature"`
Units string `json:"units"`
Condition string `json:"condition"`
}
func weatherHandler(args any) (string, error) {
// Parse to typed struct
argsBytes, _ := json.Marshal(args)
var typedArgs WeatherArgs
if err := json.Unmarshal(argsBytes, &typedArgs); err != nil {
return "", fmt.Errorf("invalid arguments: %v", err)
}
// Default units
if typedArgs.Units == "" {
typedArgs.Units = "celsius"
}
// Your weather logic here...
response := WeatherResponse{
City: typedArgs.City,
Temperature: 22.5,
Units: typedArgs.Units,
Condition: "sunny",
}
// Return as JSON string
result, _ := json.Marshal(response)
return string(result), nil
}

Tool names from RegisterMCPTool are prefixed with bifrostInternal_ when exposed to LLMs:

Registered NameLLM Sees
calculatorbifrostInternal_calculator
get_weatherbifrostInternal_get_weather

This prevents naming conflicts with tools from external MCP servers.


Return errors from your handler to indicate tool execution failures:

func myHandler(args any) (string, error) {
// Validation errors
if args == nil {
return "", fmt.Errorf("arguments required")
}
// Business logic errors
if someCondition {
return "", fmt.Errorf("operation not permitted: %s", reason)
}
// External service errors
result, err := callExternalService()
if err != nil {
return "", fmt.Errorf("service error: %w", err)
}
return result, nil
}

Errors are returned to the LLM as tool error messages, allowing it to handle the failure gracefully.


Since tools run in-process, they can access your application’s state:

type AppContext struct {
DB *sql.DB
Cache *redis.Client
UserID string
SessionID string
}
func createUserTool(appCtx *AppContext) func(args any) (string, error) {
return func(args any) (string, error) {
// Access database
rows, err := appCtx.DB.Query("SELECT * FROM users WHERE id = ?", appCtx.UserID)
if err != nil {
return "", err
}
defer rows.Close()
// Access cache
cached, _ := appCtx.Cache.Get(context.Background(), "user:"+appCtx.UserID).Result()
// Return result
return fmt.Sprintf("User data: %s", cached), nil
}
}
// Usage
appCtx := &AppContext{
DB: db,
Cache: redisClient,
UserID: "user123",
}
client.RegisterMCPTool("get_user_data", "Get current user data", createUserTool(appCtx), schema)

Validate inputs

Always validate arguments before processing:

func handler(args any) (string, error) {
m, ok := args.(map[string]interface{})
if !ok {
return "", fmt.Errorf("expected object arguments")
}
required := []string{"field1", "field2"}
for _, field := range required {
if _, exists := m[field]; !exists {
return "", fmt.Errorf("missing required field: %s", field)
}
}
// ...
}
Return structured data

Return JSON for complex responses:

func handler(args any) (string, error) {
result := map[string]interface{}{
"status": "success",
"data": []string{"item1", "item2"},
"count": 2,
}
bytes, _ := json.Marshal(result)
return string(bytes), nil
}
Handle timeouts

Use context for long-running operations:

func handler(args any) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := longOperation(ctx)
if errors.Is(err, context.DeadlineExceeded) {
return "", fmt.Errorf("operation timed out")
}
return result, err
}
Log for debugging

Add logging for troubleshooting:

func handler(args any) (string, error) {
log.Printf("Tool called with args: %+v", args)
result, err := doWork(args)
if err != nil {
log.Printf("Tool error: %v", err)
return "", err
}
log.Printf("Tool result: %s", result)
return result, nil
}

AspectTool Hosting (In-Process)External MCP Server
Latency~0.1ms (no network)10-500ms (network dependent)
DeploymentPart of your appSeparate process/service
LanguageGo onlyAny language
ConfigurationCode onlyconfig.json, API, or UI
State AccessDirect accessVia APIs
ScalingScales with appIndependent scaling

Tool Execution

Learn how tool execution works

Open →

Agent Mode

Enable auto-execution for hosted tools

Open →