---
title: "Tool Hosting"
sidebarTitle: "Tool Hosting"
description: "Register custom tools directly in your Go application without external MCP servers."
icon: "toolbox"
---
This feature is only available when using Bifrost as a **Go SDK**. It is not available in the Gateway deployment.
## Overview
**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
Bifrost automatically creates an internal MCP server (`bifrostInternal`) when you register your first tool.
---
## Basic Usage
### Step 1: Define Your Tool Schema
Create a schema that describes your tool's parameters:
```go
import "github.com/maximhq/bifrost/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"},
},
},
}
```
### Step 2: Implement the Handler
Create a function that handles tool execution:
```go
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
}
```
### Step 3: Register the Tool
Register your tool with Bifrost:
```go
import (
"context"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
)
func main() {
// Initialize Bifrost with MCP enabled (even empty config is fine)
client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
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
}
```
---
## Complete Example
Here's a complete example with multiple tools:
```go
package main
import (
"context"
"encoding/json"
"fmt"
"time"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
)
func main() {
// Initialize with empty MCP config to enable tool registration
client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
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.NewBifrostContext(context.Background(), schemas.NoDeadline), &schemas.BifrostChatRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: schemas.ChatMessageContent{
ContentStr: bifrost.Ptr("What is 15 * 7? Also, what time is it?"),
},
},
},
})
if err != nil {
panic(err)
}
// Handle tool calls...
}
func registerCalculator(client *bifrost.Bifrost) {
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 *bifrost.Bifrost) {
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)
}
}
```
---
## Typed Handlers
For better type safety, use typed structs with JSON marshaling:
```go
// 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 Naming
Tool names from `RegisterMCPTool` are prefixed with `bifrostInternal_` when exposed to LLMs:
| Registered Name | LLM Sees |
|-----------------|----------|
| `calculator` | `bifrostInternal_calculator` |
| `get_weather` | `bifrostInternal_get_weather` |
This prevents naming conflicts with tools from external MCP servers.
---
## Error Handling
Return errors from your handler to indicate tool execution failures:
```go
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.
---
## Accessing Application State
Since tools run in-process, they can access your application's state:
```go
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)
```
---
## Best Practices
Always validate arguments before processing:
```go
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 JSON for complex responses:
```go
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
}
```
Use context for long-running operations:
```go
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
}
```
Add logging for troubleshooting:
```go
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
}
```
---
## Comparison with External MCP Servers
| Aspect | Tool Hosting (In-Process) | External MCP Server |
|--------|---------------------------|---------------------|
| Latency | ~0.1ms (no network) | 10-500ms (network dependent) |
| Deployment | Part of your app | Separate process/service |
| Language | Go only | Any language |
| Configuration | Code only | config.json, API, or UI |
| State Access | Direct access | Via APIs |
| Scaling | Scales with app | Independent scaling |
---
## Next Steps
Learn how tool execution works
Enable auto-execution for hosted tools