Skip to main content

MCP Overview

MCP (Model Context Protocol) enables AI models to interact with external tools and services. Bifrost’s MCP integration provides:
  • Automatic tool discovery from external MCP servers
  • Built-in tool execution with proper error handling
  • Custom tool registration for in-process tools
  • Multiple connection types (HTTP, STDIO, SSE)
// Configure MCP during initialization
client, initErr := bifrost.Init(schemas.BifrostConfig{
    Account: &MyAccount{},
    MCPConfig: &schemas.MCPConfig{
        ClientConfigs: []schemas.MCPClientConfig{
            {
                Name:           "filesystem-tools",
                ConnectionType: schemas.MCPConnectionTypeSTDIO,
                StdioConfig: &schemas.MCPStdioConfig{
                    Command: "npx",
                    Args:    []string{"-y", "@modelcontextprotocol/server-filesystem"},
                },
            },
        },
    },
})

Basic MCP Configuration

STDIO Connection (Most Common)

Connect to MCP servers via standard input/output:
func setupMCPClient() *schemas.MCPConfig {
    return &schemas.MCPConfig{
        ClientConfigs: []schemas.MCPClientConfig{
            {
                Name:           "filesystem-tools",
                ConnectionType: schemas.MCPConnectionTypeSTDIO,
                StdioConfig: &schemas.MCPStdioConfig{
                    Command: "npx",
                    Args:    []string{"-y", "@modelcontextprotocol/server-filesystem"},
                    Envs:    []string{"FILESYSTEM_ROOT"},
                },
            },
            {
                Name:           "web-search",
                ConnectionType: schemas.MCPConnectionTypeSTDIO,
                StdioConfig: &schemas.MCPStdioConfig{
                    Command: "python",
                    Args:    []string{"-m", "web_search_mcp"},
                    Envs:    []string{"SEARCH_API_KEY"},
                },
            },
        },
    }
}

// Set environment variables
os.Setenv("FILESYSTEM_ROOT", "/safe/directory")
os.Setenv("SEARCH_API_KEY", "your-search-api-key")

client, initErr := bifrost.Init(schemas.BifrostConfig{
    Account:   &MyAccount{},
    MCPConfig: setupMCPClient(),
})

HTTP Connection

Connect to MCP servers via HTTP:
func setupHTTPMCP() *schemas.MCPConfig {
    endpoint := "http://localhost:8080/mcp"

    return &schemas.MCPConfig{
        ClientConfigs: []schemas.MCPClientConfig{
            {
                Name:             "database-tools",
                ConnectionType:   schemas.MCPConnectionTypeHTTP,
                ConnectionString: &endpoint,
            },
        },
    }
}

SSE Connection

Connect to MCP servers via Server-Sent Events:
func setupSSEMCP() *schemas.MCPConfig {
    sseEndpoint := "http://localhost:8080/mcp/sse"

    return &schemas.MCPConfig{
        ClientConfigs: []schemas.MCPClientConfig{
            {
                Name:             "realtime-data",
                ConnectionType:   schemas.MCPConnectionTypeSSE,
                ConnectionString: &sseEndpoint,
            },
        },
    }
}

Using MCP Tools

Automatic Tool Integration

MCP tools are automatically added to all requests:
// Tools from MCP servers are automatically available
response, err := client.ChatCompletionRequest(context.Background(), &schemas.BifrostRequest{
    Provider: schemas.OpenAI,
    Model:    "gpt-4o-mini",
    Input: schemas.RequestInput{
        ChatCompletionInput: &[]schemas.BifrostMessage{
            {
                Role: schemas.ModelChatMessageRoleUser,
                Content: schemas.MessageContent{ContentStr: &message},
            },
        },
    },
    // No need to specify tools - MCP tools are automatically included
})

// Check if model used any tools
if len(response.Choices) > 0 && response.Choices[0].Message.ToolCalls != nil {
    fmt.Printf("Model used %d tools\n", len(*response.Choices[0].Message.ToolCalls))
}

Manual Tool Execution

Execute MCP tools directly for security and control. You can pass tool calls directly from assistant message responses:
// Option 1: Use tool calls from assistant response
response, err := client.ChatCompletionRequest(ctx, request)
if err != nil {
    return err
}

// Execute each tool call from the assistant's response
if len(response.Choices) > 0 && response.Choices[0].Message.ToolCalls != nil {
    for _, toolCall := range *response.Choices[0].Message.ToolCalls {
        // Execute the tool call directly - gives you full control for security
        toolResult, err := client.ExecuteMCPTool(context.Background(), toolCall)
        if err != nil {
            log.Printf("Tool execution failed: %v", err)
            continue
        }

        // Process result as needed
        if toolResult.Content.ContentStr != nil {
            fmt.Printf("Tool result: %s\n", *toolResult.Content.ContentStr)
        }
    }
}

// Option 2: Create custom tool calls
toolCall := schemas.ToolCall{
    ID:   &[]string{"call_123"}[0],
    Type: &[]string{"function"}[0],
    Function: schemas.FunctionCall{
        Name:      &[]string{"read_file"}[0],
        Arguments: `{"path": "/path/to/file.txt"}`,
    },
}

// Execute the tool manually
toolResult, err := client.ExecuteMCPTool(context.Background(), toolCall)
if err != nil {
    log.Printf("Tool execution failed: %v", err)
    return
}

// Use the result
if toolResult.Content.ContentStr != nil {
    fmt.Printf("Tool result: %s\n", *toolResult.Content.ContentStr)
}
Security Note: Manual execution gives you full control over tool calls. This allows you to validate arguments, implement access controls, and audit tool usage before execution.

Custom Tool Registration

Register In-Process Tools

Register custom tools that run within your application:
// Define your tool function
func echoTool(args any) (string, error) {
    argsMap, ok := args.(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("invalid arguments")
    }

    message, ok := argsMap["message"].(string)
    if !ok {
        return "", fmt.Errorf("message parameter required")
    }

    return fmt.Sprintf("Echo: %s", message), nil
}

// Define tool schema
echoToolSchema := schemas.Tool{
    Type: "function",
    Function: schemas.Function{
        Name:        "echo",
        Description: "Echo a message back to the user",
        Parameters: schemas.FunctionParameters{
            Type: "object",
            Properties: map[string]interface{}{
                "message": map[string]interface{}{
                    "type":        "string",
                    "description": "Message to echo back",
                },
            },
            Required: []string{"message"},
        },
    },
}

// Register the tool
err := client.RegisterMCPTool("echo", "Echo a message", echoTool, echoToolSchema)
if err != nil {
    log.Printf("Failed to register tool: %v", err)
}

// Now the tool is available to all AI requests

Advanced Custom Tools

More complex tools with error handling and validation:
// Database query tool
func databaseQueryTool(args any) (string, error) {
    argsMap, ok := args.(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("invalid arguments")
    }

    query, ok := argsMap["query"].(string)
    if !ok {
        return "", fmt.Errorf("query parameter required")
    }

    // Validate query (prevent dangerous operations)
    if strings.Contains(strings.ToLower(query), "drop") ||
       strings.Contains(strings.ToLower(query), "delete") ||
       strings.Contains(strings.ToLower(query), "update") {
        return "", fmt.Errorf("only SELECT queries are allowed")
    }

    // Execute query (pseudo-code)
    db := getDatabase()
    rows, err := db.Query(query)
    if err != nil {
        return "", fmt.Errorf("query failed: %w", err)
    }
    defer rows.Close()

    // Format results as JSON
    results := []map[string]interface{}{}
    for rows.Next() {
        // Scan row data...
        row := map[string]interface{}{
            "id":   1,
            "name": "example",
        }
        results = append(results, row)
    }

    jsonData, _ := json.Marshal(results)
    return string(jsonData), nil
}

// Register database tool
dbToolSchema := schemas.Tool{
    Type: "function",
    Function: schemas.Function{
        Name:        "database_query",
        Description: "Execute a safe SELECT query on the database",
        Parameters: schemas.FunctionParameters{
            Type: "object",
            Properties: map[string]interface{}{
                "query": map[string]interface{}{
                    "type":        "string",
                    "description": "SQL SELECT query to execute",
                },
            },
            Required: []string{"query"},
        },
    },
}

err := client.RegisterMCPTool("database_query", "Query database", databaseQueryTool, dbToolSchema)

Tool Discovery and Filtering

Tool Filtering by Client (Config Level)

Control which tools from each MCP client are available at the configuration level:
mcpConfig := &schemas.MCPConfig{
    ClientConfigs: []schemas.MCPClientConfig{
        {
            Name:           "filesystem-tools",
            ConnectionType: schemas.MCPConnectionTypeSTDIO,
            StdioConfig: &schemas.MCPStdioConfig{
                Command: "npx",
                Args:    []string{"-y", "@modelcontextprotocol/server-filesystem"},
            },
            // Whitelist approach: Only allow specific tools
            ToolsToExecute: []string{"read_file", "list_directory"},
        },
        {
            Name:           "web-tools",
            ConnectionType: schemas.MCPConnectionTypeSTDIO,
            StdioConfig: &schemas.MCPStdioConfig{
                Command: "npx",
                Args:    []string{"-y", "@modelcontextprotocol/server-web"},
            },
            // Blacklist approach: Block dangerous tools
            ToolsToSkip: []string{"delete_page", "modify_content"},
        },
    },
}
Filtering Rules:
  • ToolsToExecute: Whitelist - only these tools are available (overrides ToolsToSkip)
  • ToolsToSkip: Blacklist - all tools except these are available
  • If both are specified, ToolsToExecute takes precedence

Context-Based Tool Filtering (Request Level)

Filter tools at runtime for specific requests using context keys:
import "context"

// Whitelist specific clients (only these clients' tools will be available)
ctx := context.WithValue(context.Background(), "mcp-include-clients", []string{"filesystem-tools", "database-client"})

response, err := client.ChatCompletionRequest(ctx, &schemas.BifrostRequest{
    Provider: schemas.OpenAI,
    Model:    "gpt-4o-mini",
    Input:    input, // your input here
})

// Blacklist specific clients (all tools except these clients' tools will be available)
ctx = context.WithValue(context.Background(), "mcp-exclude-clients", []string{"web-tools", "admin-tools"})

response, err = client.ChatCompletionRequest(ctx, &schemas.BifrostRequest{
    Provider: schemas.Anthropic,
    Model:    "claude-3-sonnet-20240229",
    Input:    input, // your input here
})

// Combine both approaches for fine-grained control
func createRestrictedContext() context.Context {
    ctx := context.Background()

    // Only allow safe tools for user-facing operations
    ctx = context.WithValue(ctx, "mcp-include-clients", []string{"search-tools", "calculator"})

    return ctx
}

// Use in production
userCtx := createRestrictedContext()
response, err := client.ChatCompletionRequest(userCtx, userRequest)
Context Filtering Rules:
  • mcp-include-clients: Whitelist - only tools from these named MCP clients are available
  • mcp-exclude-clients: Blacklist - tools from these named MCP clients are excluded
  • If both are specified, mcp-include-clients takes precedence Similarly you can pass values for mcp-include-tools and mcp-exclude-tools to filter tools at runtime.
  • These filters work at runtime and can be different for each request
  • Useful for user-based permissions, request-specific security, or A/B testing different tool sets

Dynamic Client Management

Adding Clients at Runtime

Add new MCP clients after initialization:
// Add a new filesystem client
newClientConfig := schemas.MCPClientConfig{
    Name:           "new-filesystem",
    ConnectionType: schemas.MCPConnectionTypeSTDIO,
    StdioConfig: &schemas.MCPStdioConfig{
        Command: "npx",
        Args:    []string{"-y", "@modelcontextprotocol/server-filesystem"},
        Envs:    []string{"FILESYSTEM_ROOT"},
    },
}

err := client.AddMCPClient(newClientConfig)
if err != nil {
    log.Printf("Failed to add MCP client: %v", err)
}

// Add HTTP client
httpEndpoint := "http://localhost:8080/mcp"
httpClientConfig := schemas.MCPClientConfig{
    Name:             "api-tools",
    ConnectionType:   schemas.MCPConnectionTypeHTTP,
    ConnectionString: &httpEndpoint,
}

err = client.AddMCPClient(httpClientConfig)
if err != nil {
    log.Printf("Failed to add HTTP MCP client: %v", err)
}

Removing Clients

Remove MCP clients when no longer needed:
// Remove a specific client
err := client.RemoveMCPClient("filesystem-tools")
if err != nil {
    log.Printf("Failed to remove MCP client: %v", err)
}

// The client will be disconnected and its tools will no longer be available

Editing Client Tool Filters

Dynamically modify which tools are available from a client:
// Allow only specific tools from a client
toolsToAdd := []string{"read_file", "list_directory"}
toolsToRemove := []string{"write_file", "delete_file"}

err := client.EditMCPClientTools("filesystem-tools", toolsToAdd, toolsToRemove)
if err != nil {
    log.Printf("Failed to edit client tools: %v", err)
}

// Clear all restrictions (allow all tools)
err = client.EditMCPClientTools("filesystem-tools", []string{}, []string{})
if err != nil {
    log.Printf("Failed to clear tool restrictions: %v", err)
}

// Block specific dangerous tools
dangerousTools := []string{"delete_file", "format_disk", "system_shutdown"}
err = client.EditMCPClientTools("filesystem-tools", []string{}, dangerousTools)
if err != nil {
    log.Printf("Failed to block dangerous tools: %v", err)
}

Listing All Clients

Get information about all connected MCP clients:
import "strings"

clients, err := client.GetMCPClients()
if err != nil {
    log.Printf("Failed to get MCP clients: %v", err)
    return
}

for _, mcpClient := range clients {
    fmt.Printf("Client: %s\n", mcpClient.Name)
    fmt.Printf("  Type: %s\n", mcpClient.Config.ConnectionType)
    fmt.Printf("  State: %s\n", mcpClient.State)
    
    if mcpClient.Config.ConnectionString != nil {
        fmt.Printf("  URL: %s\n", *mcpClient.Config.ConnectionString)
    }
    
    if mcpClient.Config.StdioConfig != nil {
        cmdString := fmt.Sprintf("%s %s", mcpClient.Config.StdioConfig.Command, 
                                strings.Join(mcpClient.Config.StdioConfig.Args, " "))
        fmt.Printf("  Command: %s\n", cmdString)
    }
    
    fmt.Printf("  Tools: %d available\n", len(mcpClient.Tools))
    for _, toolName := range mcpClient.Tools {
        fmt.Printf("    - %s\n", toolName)
    }
    fmt.Println()
}

// Filter clients by connection state
connectedClients := []schemas.MCPClient{}
disconnectedClients := []schemas.MCPClient{}

for _, mcpClient := range clients {
    if mcpClient.State == schemas.MCPConnectionStateConnected {
        connectedClients = append(connectedClients, mcpClient)
    } else {
        disconnectedClients = append(disconnectedClients, mcpClient)
    }
}

fmt.Printf("Connected clients: %d\n", len(connectedClients))
fmt.Printf("Disconnected clients: %d\n", len(disconnectedClients))

Reconnecting Disconnected Clients

Reconnect clients that have lost their connection:
// Reconnect a specific client
err := client.ReconnectMCPClient("web-search")
if err != nil {
    log.Printf("Failed to reconnect MCP client: %v", err)
}

// Check client status before reconnecting
clients, err := client.GetMCPClients()
if err == nil {
    for _, mcpClient := range clients {
        if mcpClient.State != schemas.MCPConnectionStateConnected {
            fmt.Printf("Client %s is %s, attempting reconnect...\n", mcpClient.Name, mcpClient.State)
            if err := client.ReconnectMCPClient(mcpClient.Name); err != nil {
                log.Printf("Failed to reconnect %s: %v", mcpClient.Name, err)
            } else {
                fmt.Printf("Successfully reconnected %s\n", mcpClient.Name)
            }
        }
    }
}

Dynamic Client Management Example

Complete example showing dynamic client lifecycle:
func manageMCPClients(client *bifrost.Bifrost) {
    // 1. Add clients based on runtime conditions
    if needsFileAccess() {
        fileClientConfig := schemas.MCPClientConfig{
            Name:           "runtime-filesystem",
            ConnectionType: schemas.MCPConnectionTypeSTDIO,
            StdioConfig: &schemas.MCPStdioConfig{
                Command: "npx",
                Args:    []string{"-y", "@modelcontextprotocol/server-filesystem"},
                Envs:    []string{"FILESYSTEM_ROOT"},
            },
        }
        
        if err := client.AddMCPClient(fileClientConfig); err != nil {
            log.Printf("Failed to add filesystem client: %v", err)
        }
    }

    // 2. Configure tool access based on user permissions
    if isRestrictedUser() {
        // Only allow safe read operations
        safeTools := []string{"read_file", "list_directory"}
        err := client.EditMCPClientTools("runtime-filesystem", safeTools, []string{})
        if err != nil {
            log.Printf("Failed to restrict tools: %v", err)
        }
    }

    // 3. Monitor and maintain connections
    go func() {
        ticker := time.NewTicker(30 * time.Second)
        defer ticker.Stop()
        
        for range ticker.C {
            clients, err := client.GetMCPClients()
            if err != nil {
                continue
            }
            
            for _, mcpClient := range clients {
                if mcpClient.State != schemas.MCPConnectionStateConnected {
                    log.Printf("Detected disconnected client: %s (state: %s)", mcpClient.Name, mcpClient.State)
                    if err := client.ReconnectMCPClient(mcpClient.Name); err != nil {
                        log.Printf("Failed to reconnect %s: %v", mcpClient.Name, err)
                    }
                }
            }
        }
    }()

    // 4. Cleanup when done
    defer func() {
        // Remove temporary clients
        if err := client.RemoveMCPClient("runtime-filesystem"); err != nil {
            log.Printf("Failed to remove temporary client: %v", err)
        }
    }()
}

func needsFileAccess() bool {
    // Your logic to determine if file access is needed
    return true
}

func isRestrictedUser() bool {
    // Your logic to determine user permissions
    return false
}

🔄 Multi-Turn Tool Conversations

Handling Tool Call Loops

Implement proper tool calling conversations:
func handleToolConversation(client *bifrost.Bifrost, initialMessage string) {
    conversation := []schemas.BifrostMessage{
        {
            Role: schemas.ModelChatMessageRoleUser,
            Content: schemas.MessageContent{ContentStr: &initialMessage},
        },
    }

    maxTurns := 10
    for turn := 0; turn < maxTurns; turn++ {
        response, err := client.ChatCompletionRequest(context.Background(), &schemas.BifrostRequest{
            Provider: schemas.OpenAI,
            Model:    "gpt-4o-mini",
            Input: schemas.RequestInput{
                ChatCompletionInput: &conversation,
            },
        })

        if err != nil {
            log.Printf("Request failed: %v", err)
            return
        }

        choice := response.Choices[0]

        // Add assistant's response to conversation
        conversation = append(conversation, choice.Message)

        // Check if model wants to call tools
        if choice.Message.ToolCalls != nil {
            // Execute all tool calls
            for _, toolCall := range *choice.Message.ToolCalls {
                toolResult, err := client.ExecuteMCPTool(context.Background(), toolCall)
                if err != nil {
                    log.Printf("Tool execution failed: %v", err)
                    continue
                }

                // Add tool result to conversation
                conversation = append(conversation, *toolResult)
            }

            // Continue conversation with tool results
            continue
        }

        // No more tool calls - conversation is complete
        if choice.Message.Content.ContentStr != nil {
            fmt.Printf("Final response: %s\n", *choice.Message.Content.ContentStr)
        }
        break
    }
}

// Usage
handleToolConversation(client, "Analyze the files in the current directory and summarize what the project does")

📊 MCP Monitoring and Debugging

Tool Execution Monitoring

Track tool usage and performance:
type MCPMonitoringPlugin struct {
    toolCalls map[string]int
    errors    map[string]int
    mu        sync.RWMutex
}

func (p *MCPMonitoringPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
    // Add monitoring context
    *ctx = context.WithValue(*ctx, "mcp_monitor_start", time.Now())
    return req, nil, nil
}

func (p *MCPMonitoringPlugin) PostHook(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
    if result != nil && len(result.Choices) > 0 && result.Choices[0].Message.ToolCalls != nil {
        p.mu.Lock()
        for _, toolCall := range *result.Choices[0].Message.ToolCalls {
            if toolCall.Function.Name != nil {
                p.toolCalls[*toolCall.Function.Name]++
            }
        }
        p.mu.Unlock()
    }

    return result, err, nil
}

// Get monitoring data
func (p *MCPMonitoringPlugin) GetToolStats() map[string]int {
    p.mu.RLock()
    defer p.mu.RUnlock()

    stats := make(map[string]int)
    for tool, count := range p.toolCalls {
        stats[tool] = count
    }
    return stats
}

Debug Tool Execution

Enable detailed logging for MCP operations:
// Create logger that shows MCP operations
logger := log.New(os.Stdout, "[MCP] ", log.LstdFlags|log.Lshortfile)

client, initErr := bifrost.Init(schemas.BifrostConfig{
    Account:   &MyAccount{},
    Logger:    customLogger, // Use custom logger for MCP debug info
    MCPConfig: mcpConfig,
})

// MCP operations will be logged with detailed information

🧪 Testing MCP Integration

Unit Testing Custom Tools

Test your custom tools in isolation:
func TestEchoTool(t *testing.T) {
    args := map[string]interface{}{
        "message": "Hello, World!",
    }

    result, err := echoTool(args)
    assert.NoError(t, err)
    assert.Equal(t, "Echo: Hello, World!", result)

    // Test error case
    invalidArgs := map[string]interface{}{
        "wrong_param": "value",
    }

    _, err = echoTool(invalidArgs)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "message parameter required")
}

Integration Testing with MCP

Test MCP integration with real tools:
func TestMCPIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping MCP integration test")
    }

    // Setup MCP client with echo tool
    client, initErr := bifrost.Init(schemas.BifrostConfig{
        Account: &TestAccount{},
        MCPConfig: &schemas.MCPConfig{
            ClientConfigs: []schemas.MCPClientConfig{
                // Configure test MCP server
            },
        },
    })
    require.Nil(t, initErr)
    defer client.Cleanup()

    // Register test tool
    err = client.RegisterMCPTool("test_echo", "Test echo", echoTool, echoToolSchema)
    require.NoError(t, err)

    // Test tool is available in requests
    message := "Use the echo tool to repeat this message"
    response, err := client.ChatCompletionRequest(context.Background(), &schemas.BifrostRequest{
        Provider: schemas.OpenAI,
        Model:    "gpt-4o-mini",
        Input: schemas.RequestInput{
            ChatCompletionInput: &[]schemas.BifrostMessage{
                {Role: schemas.ModelChatMessageRoleUser, Content: schemas.MessageContent{ContentStr: &message}},
            },
        },
    })

    assert.NoError(t, err)
    assert.NotNil(t, response)

    // Check if tool was called
    if len(response.Choices) > 0 && response.Choices[0].Message.ToolCalls != nil {
        foundEchoTool := false
        for _, toolCall := range *response.Choices[0].Message.ToolCalls {
            if toolCall.Function.Name != nil && *toolCall.Function.Name == "test_echo" {
                foundEchoTool = true
                break
            }
        }
        assert.True(t, foundEchoTool, "Echo tool should have been called")
    }
}

Architecture: For MCP system design and integration details, see Architecture Documentation.