Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,17 @@ func (bifrost *Bifrost) GetMCPClients() ([]schemas.MCPClient, error) {
return clientsInConfig, nil
}

// GetAvailableTools returns the available tools for the given context.
//
// Returns:
// - []schemas.ChatTool: List of available tools
func (bifrost *Bifrost) GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool {
if bifrost.mcpManager == nil {
return nil
}
return bifrost.mcpManager.GetAvailableTools(ctx)
}

// AddMCPClient adds a new MCP client to the Bifrost instance.
// This allows for dynamic MCP client management at runtime.
//
Expand Down Expand Up @@ -2079,6 +2090,7 @@ func executeRequestWithRetries[T any](

// Calculate and apply backoff
backoff := calculateBackoff(attempts-1, config)
logger.Debug("sleeping for %s before retry", backoff)
time.Sleep(backoff)
}

Expand Down
12 changes: 9 additions & 3 deletions core/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ func NewMCPManager(ctx context.Context, config schemas.MCPConfig, logger schemas
}
manager.toolsHandler = NewToolsManager(config.ToolManagerConfig, manager, config.FetchNewRequestIDFunc)
// Process client configs: create client map entries and establish connections
for _, clientConfig := range config.ClientConfigs {
if err := manager.AddClient(clientConfig); err != nil {
logger.Warn(fmt.Sprintf("%s Failed to add MCP client %s: %v", MCPLogPrefix, clientConfig.Name, err))
if len(config.ClientConfigs) > 0 {
for _, clientConfig := range config.ClientConfigs {
if err := manager.AddClient(clientConfig); err != nil {
logger.Warn(fmt.Sprintf("%s Failed to add MCP client %s: %v", MCPLogPrefix, clientConfig.Name, err))
}
}
}
logger.Info(MCPLogPrefix + " MCP Manager initialized")
Expand All @@ -103,6 +105,10 @@ func (m *MCPManager) AddToolsToRequest(ctx context.Context, req *schemas.Bifrost
return m.toolsHandler.ParseAndAddToolsToRequest(ctx, req)
}

func (m *MCPManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool {
return m.toolsHandler.GetAvailableTools(ctx)
}

// ExecuteTool executes a single tool call from a chat assistant message.
// It handles tool execution, error handling, and returns the result as a chat message.
//
Expand Down
148 changes: 118 additions & 30 deletions core/mcp/toolmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -73,21 +74,8 @@ func NewToolsManager(config *schemas.MCPToolManagerConfig, clientManager ClientM
return manager
}

// ParseAndAddToolsToRequest parses the available tools per client and adds them to the Bifrost request.
//
// Parameters:
// - ctx: Execution context
// - req: Bifrost request
// - availableToolsPerClient: Map of client name to its available tools
//
// Returns:
// - *schemas.BifrostRequest: Bifrost request with MCP tools added
func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schemas.BifrostRequest) *schemas.BifrostRequest {
// MCP is only supported for chat and responses requests
if req.ChatRequest == nil && req.ResponsesRequest == nil {
return req
}

// GetAvailableTools returns the available tools for the given context.
func (m *ToolsManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool {
availableToolsPerClient := m.clientManager.GetToolPerClient(ctx)
// Flatten tools from all clients into a single slice, avoiding duplicates
var availableTools []schemas.ChatTool
Expand Down Expand Up @@ -133,8 +121,99 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem
}
}

return availableTools
}

// buildIntegrationDuplicateCheckMap builds a map of tool names to check for duplicates
// based on the integration user agent. This includes both direct tool names and
// integration-specific naming patterns from existing tools in the request.
//
// Parameters:
// - existingTools: List of existing tools in the request
// - integrationUserAgent: Integration user agent string (e.g., "claude-cli")
// - availableToolsPerClient: Map of client names to their available tools (for reverse pattern matching)
//
// Returns:
// - map[string]bool: Map of tool names/patterns to check against
func buildIntegrationDuplicateCheckMap(existingTools []schemas.ChatTool, integrationUserAgent string) map[string]bool {
duplicateCheckMap := make(map[string]bool)

// Add direct tool names
for _, tool := range existingTools {
if tool.Function != nil && tool.Function.Name != "" {
duplicateCheckMap[tool.Function.Name] = true
}
}

// Add integration-specific patterns from existing tools
switch integrationUserAgent {
case "claude-cli":
// Claude CLI uses pattern: mcp__{foreign_name}__{tool_name}
// The middle part is a foreign name we cannot check for, so we extract the last part
// Examples:
// mcp__bifrost__executeToolCode -> executeToolCode
// mcp__bifrost__listToolFiles -> listToolFiles
// mcp__bifrost__readToolFile -> readToolFile
// mcp__calculator__calculator_add -> calculator_add
for _, tool := range existingTools {
if tool.Function != nil && tool.Function.Name != "" {
existingToolName := tool.Function.Name
// Check if existing tool matches Claude CLI pattern: mcp__*__{tool_name}
if strings.HasPrefix(existingToolName, "mcp__") {
// Split on __ and take the last entry (the tool_name)
parts := strings.Split(existingToolName, "__")
if len(parts) >= 3 {
toolName := parts[len(parts)-1] // Last part is the tool name
// Map Claude CLI pattern back to our tool name format
// This handles both regular MCP tools and code mode tools
if toolName != "" {
duplicateCheckMap[toolName] = true
// Also keep the original pattern for direct matching
duplicateCheckMap[existingToolName] = true
}
}
}
}
}
// Add more integration-specific patterns here as needed
// case "another-integration":
// // Add patterns for other integrations
}

return duplicateCheckMap
}

// ParseAndAddToolsToRequest parses the available tools per client and adds them to the Bifrost request.
//
// Parameters:
// - ctx: Execution context
// - req: Bifrost request
// - availableToolsPerClient: Map of client name to its available tools
//
// Returns:
// - *schemas.BifrostRequest: Bifrost request with MCP tools added
func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schemas.BifrostRequest) *schemas.BifrostRequest {
// MCP is only supported for chat and responses requests
if req.ChatRequest == nil && req.ResponsesRequest == nil {
return req
}

availableTools := m.GetAvailableTools(ctx)

if len(availableTools) == 0 {
return req
}

// Get integration user agent for duplicate checking
var integrationUserAgentStr string
integrationUserAgent := ctx.Value(schemas.BifrostContextKey("integration-user-agent"))
if integrationUserAgent != nil {
if str, ok := integrationUserAgent.(string); ok {
integrationUserAgentStr = str
}
}

if len(availableTools) > 0 {
logger.Debug(fmt.Sprintf("%s Adding %d MCP tools to request from %d clients", MCPLogPrefix, len(availableTools), len(availableToolsPerClient)))
switch req.RequestType {
case schemas.ChatCompletionRequest, schemas.ChatCompletionStreamRequest:
// Only allocate new Params if it's nil to preserve caller-supplied settings
Expand All @@ -144,13 +223,8 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem

tools := req.ChatRequest.Params.Tools

// Create a map of existing tool names for O(1) lookup
existingToolsMap := make(map[string]bool)
for _, tool := range tools {
if tool.Function != nil && tool.Function.Name != "" {
existingToolsMap[tool.Function.Name] = true
}
}
// Build integration-aware duplicate check map
duplicateCheckMap := buildIntegrationDuplicateCheckMap(tools, integrationUserAgentStr)

// Add MCP tools that are not already present
for _, mcpTool := range availableTools {
Expand All @@ -159,10 +233,13 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem
continue
}

if !existingToolsMap[mcpTool.Function.Name] {
toolName := mcpTool.Function.Name

// Check for duplicates using integration-aware logic
if !duplicateCheckMap[toolName] {
tools = append(tools, mcpTool)
// Update the map to prevent duplicates within MCP tools as well
existingToolsMap[mcpTool.Function.Name] = true
duplicateCheckMap[toolName] = true
}
}
req.ChatRequest.Params.Tools = tools
Expand All @@ -174,22 +251,33 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem

tools := req.ResponsesRequest.Params.Tools

// Create a map of existing tool names for O(1) lookup
existingToolsMap := make(map[string]bool)
// Convert Responses tools to ChatTool format for duplicate checking
existingChatTools := make([]schemas.ChatTool, 0, len(tools))
for _, tool := range tools {
if tool.Name != nil {
existingToolsMap[*tool.Name] = true
existingChatTools = append(existingChatTools, schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: *tool.Name,
},
})
}
}

// Build integration-aware duplicate check map
duplicateCheckMap := buildIntegrationDuplicateCheckMap(existingChatTools, integrationUserAgentStr)

// Add MCP tools that are not already present
for _, mcpTool := range availableTools {
// Skip tools with nil Function or empty Name
if mcpTool.Function == nil || mcpTool.Function.Name == "" {
continue
}

if !existingToolsMap[mcpTool.Function.Name] {
toolName := mcpTool.Function.Name

// Check for duplicates using integration-aware logic
if !duplicateCheckMap[toolName] {
responsesTool := mcpTool.ToResponsesTool()
// Skip if the converted tool has nil Name
if responsesTool.Name == nil {
Expand All @@ -198,7 +286,7 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem

tools = append(tools, *responsesTool)
// Update the map to prevent duplicates within MCP tools as well
existingToolsMap[*responsesTool.Name] = true
duplicateCheckMap[toolName] = true
}
}
req.ResponsesRequest.Params.Tools = tools
Expand Down
2 changes: 0 additions & 2 deletions core/mcp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,6 @@ func canAutoExecuteTool(toolName string, config schemas.MCPClientConfig) bool {
func shouldSkipToolForRequest(ctx context.Context, clientName, toolName string) bool {
includeTools := ctx.Value(MCPContextKeyIncludeTools)

logger.Debug(fmt.Sprintf("%s Checking if tool %s should be skipped for request: %v", MCPLogPrefix, toolName, includeTools))

if includeTools != nil {
// Try []string first (preferred type)
if includeToolsList, ok := includeTools.([]string); ok {
Expand Down
2 changes: 1 addition & 1 deletion docs/features/governance/virtual-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ curl -X POST http://localhost:8080/v1/chat/completions \
{
"error": {
"type": "virtual_key_required",
"message": "x-bf-vk header is missing"
"message": "virtual key is missing in headers"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion plugins/governance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ func (p *GovernancePlugin) PreHook(ctx *context.Context, req *schemas.BifrostReq
Type: bifrost.Ptr("virtual_key_required"),
StatusCode: bifrost.Ptr(400),
Error: &schemas.ErrorField{
Message: "x-bf-vk header is missing",
Message: "virtual key is missing in headers",
},
},
}, nil
Expand Down
7 changes: 4 additions & 3 deletions plugins/logging/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ func (p *LoggerPlugin) cleanupWorker() {

// cleanupOldProcessingLogs removes processing logs older than 30 minutes
func (p *LoggerPlugin) cleanupOldProcessingLogs() {
// Calculate timestamp for 30 minutes ago
thirtyMinutesAgo := time.Now().Add(-1 * 30 * time.Minute)
// Calculate timestamp for 30 minutes ago in UTC to match log entry timestamps
thirtyMinutesAgo := time.Now().UTC().Add(-1 * 30 * time.Minute)
p.logger.Debug("cleaning up old processing logs before %s", thirtyMinutesAgo)
// Delete processing logs older than 30 minutes using the store
if err := p.store.Flush(p.ctx, thirtyMinutesAgo); err != nil {
p.logger.Warn("failed to cleanup old processing logs: %v", err)
Expand Down Expand Up @@ -305,7 +306,7 @@ func (p *LoggerPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest
CreatedAt: msg.Timestamp,
}
p.logCallback(initialEntry)
}
}
}
}(logMsg)

Expand Down
1 change: 0 additions & 1 deletion transports/bifrost-http/handlers/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,6 @@ func (h *CompletionHandler) handleStreamingResponse(ctx *fasthttp.RequestCtx, ge
ctx.SetContentType("text/event-stream")
ctx.Response.Header.Set("Cache-Control", "no-cache")
ctx.Response.Header.Set("Connection", "keep-alive")
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")

// Get the streaming channel
stream, bifrostErr := getStream()
Expand Down
Loading