Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 14 additions & 11 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type Bifrost struct {
responseStreamPool sync.Pool // Pool for response stream channels, initial pool size is set in Init
pluginPipelinePool sync.Pool // Pool for PluginPipeline objects
bifrostRequestPool sync.Pool // Pool for BifrostRequest objects
mcpRequestPool sync.Pool // Pool for BifrostMCPRequest objects
oauth2Provider schemas.OAuth2Provider // OAuth provider instance
logger schemas.Logger // logger instance, default logger is used if not provided
tracer atomic.Value // tracer for distributed tracing (stores schemas.Tracer, NoOpTracer if not configured)
mcpManager *mcp.MCPManager // MCP integration manager (nil if MCP not configured)
Expand Down Expand Up @@ -171,15 +173,16 @@ func Init(ctx context.Context, config schemas.BifrostConfig) (*Bifrost, error) {

bifrostCtx, cancel := schemas.NewBifrostContextWithCancel(ctx)
bifrost := &Bifrost{
ctx: bifrostCtx,
cancel: cancel,
account: config.Account,
llmPlugins: atomic.Pointer[[]schemas.LLMPlugin]{},
mcpPlugins: atomic.Pointer[[]schemas.MCPPlugin]{},
requestQueues: sync.Map{},
waitGroups: sync.Map{},
keySelector: config.KeySelector,
logger: config.Logger,
ctx: bifrostCtx,
cancel: cancel,
account: config.Account,
llmPlugins: atomic.Pointer[[]schemas.LLMPlugin]{},
mcpPlugins: atomic.Pointer[[]schemas.MCPPlugin]{},
requestQueues: sync.Map{},
waitGroups: sync.Map{},
keySelector: config.KeySelector,
oauth2Provider: config.OAuth2Provider,
logger: config.Logger,
}
bifrost.tracer.Store(&tracerWrapper{tracer: tracer})
bifrost.llmPlugins.Store(&config.LLMPlugins)
Expand Down Expand Up @@ -266,7 +269,7 @@ func Init(ctx context.Context, config schemas.BifrostConfig) (*Bifrost, error) {
bifrost.releasePluginPipeline(pp)
}
}
bifrost.mcpManager = mcp.NewMCPManager(bifrostCtx, mcpConfig, bifrost.logger)
bifrost.mcpManager = mcp.NewMCPManager(bifrostCtx, mcpConfig, bifrost.oauth2Provider, bifrost.logger)
bifrost.logger.Info("MCP integration initialized successfully")
})
}
Expand Down Expand Up @@ -2739,7 +2742,7 @@ func (bifrost *Bifrost) AddMCPClient(config schemas.MCPClientConfig) error {
bifrost.releasePluginPipeline(pp)
}
}
bifrost.mcpManager = mcp.NewMCPManager(bifrost.ctx, mcpConfig, bifrost.logger)
bifrost.mcpManager = mcp.NewMCPManager(bifrost.ctx, mcpConfig, bifrost.oauth2Provider, bifrost.logger)
})
}

Expand Down
2 changes: 1 addition & 1 deletion core/internal/mcptests/agent_request_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func setupMCPManagerWithRequestIDFunc(t *testing.T, fetchNewRequestIDFunc func(c

// Create MCP manager
logger := &testLogger{t: t}
manager := mcp.NewMCPManager(context.Background(), *mcpConfig, logger)
manager := mcp.NewMCPManager(context.Background(), *mcpConfig, nil, logger)

// Cleanup
t.Cleanup(func() {
Expand Down
2 changes: 1 addition & 1 deletion core/internal/mcptests/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -1471,7 +1471,7 @@ func setupMCPManager(t *testing.T, clientConfigs ...schemas.MCPClientConfig) *mc

// Create MCP manager
logger := &testLogger{t: t}
manager := mcpcore.NewMCPManager(context.Background(), *mcpConfig, logger)
manager := mcpcore.NewMCPManager(context.Background(), *mcpConfig, nil, logger)

// Cleanup
t.Cleanup(func() {
Expand Down
102 changes: 83 additions & 19 deletions core/mcp/clientmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,57 @@ func (m *MCPManager) AddClient(config schemas.MCPClientConfig) error {

// Create placeholder entry
m.clientMap[config.ID] = &schemas.MCPClientState{
Name: config.Name,
ExecutionConfig: config,
ToolMap: make(map[string]schemas.ChatTool),
Name: config.Name,
ExecutionConfig: config,
ToolMap: make(map[string]schemas.ChatTool),
ToolNameMapping: make(map[string]string),
}

// Temporarily unlock for the connection attempt
// This is to avoid deadlocks when the connection attempt is made
m.mu.Unlock()

// Connect using the copied config
if err := m.connectToMCPClient(configCopy); err != nil {
// Re-lock to clean up the failed entry
m.mu.Lock()
delete(m.clientMap, config.ID)
m.mu.Unlock()
return fmt.Errorf("failed to connect to MCP client %s: %w", config.Name, err)
}

return nil
}

// AddClientInMemory adds an MCP client to memory and connects it, but does NOT persist to database.
// This is used when the MCP config already exists in the database (e.g., after OAuth completion).
//
// Parameters:
// - config: MCP client configuration
//
// Returns:
// - error: Any error that occurred during client addition or connection
func (m *MCPManager) AddClientInMemory(config schemas.MCPClientConfig) error {
if err := validateMCPClientConfig(&config); err != nil {
return fmt.Errorf("invalid MCP client configuration: %w", err)
}

// Make a copy of the config to use after unlocking
configCopy := config

m.mu.Lock()

if _, ok := m.clientMap[config.ID]; ok {
m.mu.Unlock()
return fmt.Errorf("client %s already exists", config.Name)
}

// Create placeholder entry
m.clientMap[config.ID] = &schemas.MCPClientState{
Name: config.Name,
ExecutionConfig: config,
ToolMap: make(map[string]schemas.ChatTool),
ToolNameMapping: make(map[string]string),
}

// Temporarily unlock for the connection attempt
Expand Down Expand Up @@ -392,9 +440,10 @@ func (m *MCPManager) connectToMCPClient(config schemas.MCPClientConfig) error {
}
// Create new client entry with configuration
m.clientMap[config.ID] = &schemas.MCPClientState{
Name: config.Name,
ExecutionConfig: config,
ToolMap: make(map[string]schemas.ChatTool),
Name: config.Name,
ExecutionConfig: config,
ToolMap: make(map[string]schemas.ChatTool),
ToolNameMapping: make(map[string]string),
ConnectionInfo: schemas.MCPClientConnectionInfo{
Type: config.ConnectionType,
},
Expand All @@ -410,13 +459,13 @@ func (m *MCPManager) connectToMCPClient(config schemas.MCPClientConfig) error {
logger.Debug(fmt.Sprintf("%s [%s] Creating %s connection...", MCPLogPrefix, config.Name, config.ConnectionType))
switch config.ConnectionType {
case schemas.MCPConnectionTypeHTTP:
externalClient, connectionInfo, err = m.createHTTPConnection(config)
externalClient, connectionInfo, err = m.createHTTPConnection(m.ctx, config)
case schemas.MCPConnectionTypeSTDIO:
externalClient, connectionInfo, err = m.createSTDIOConnection(config)
externalClient, connectionInfo, err = m.createSTDIOConnection(m.ctx, config)
case schemas.MCPConnectionTypeSSE:
externalClient, connectionInfo, err = m.createSSEConnection(config)
externalClient, connectionInfo, err = m.createSSEConnection(m.ctx, config)
case schemas.MCPConnectionTypeInProcess:
externalClient, connectionInfo, err = m.createInProcessConnection(config)
externalClient, connectionInfo, err = m.createInProcessConnection(m.ctx, config)
default:
return fmt.Errorf("unknown connection type: %s", config.ConnectionType)
}
Expand Down Expand Up @@ -496,11 +545,12 @@ func (m *MCPManager) connectToMCPClient(config schemas.MCPClientConfig) error {

// Retrieve tools from the external server (this also requires network I/O)
logger.Debug(fmt.Sprintf("%s [%s] Retrieving tools...", MCPLogPrefix, config.Name))
tools, err := retrieveExternalTools(ctx, externalClient, config.Name)
tools, toolNameMapping, err := retrieveExternalTools(ctx, externalClient, config.Name)
if err != nil {
logger.Warn("%s Failed to retrieve tools from %s: %v", MCPLogPrefix, config.Name, err)
// Continue with connection even if tool retrieval fails
tools = make(map[string]schemas.ChatTool)
toolNameMapping = make(map[string]string)
}
logger.Debug(fmt.Sprintf("%s [%s] Retrieved %d tools", MCPLogPrefix, config.Name, len(tools)))

Expand All @@ -525,6 +575,9 @@ func (m *MCPManager) connectToMCPClient(config schemas.MCPClientConfig) error {
client.ToolMap[toolName] = tool
}

// Store tool name mapping for execution (sanitized_name -> original_mcp_name)
client.ToolNameMapping = toolNameMapping

logger.Debug(fmt.Sprintf("%s [%s] Registering %d tools. Client config - ID: %s, Name: %s, IsCodeModeClient: %v", MCPLogPrefix, config.Name, len(tools), config.ID, config.Name, config.IsCodeModeClient))
logger.Info(fmt.Sprintf("%s Connected to MCP server '%s'", MCPLogPrefix, config.Name))
} else {
Expand Down Expand Up @@ -563,7 +616,7 @@ func (m *MCPManager) connectToMCPClient(config schemas.MCPClientConfig) error {
}

// createHTTPConnection creates an HTTP-based MCP client connection without holding locks.
func (m *MCPManager) createHTTPConnection(config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
func (m *MCPManager) createHTTPConnection(ctx context.Context, config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
if config.ConnectionString == nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("HTTP connection string is required")
}
Expand All @@ -574,8 +627,13 @@ func (m *MCPManager) createHTTPConnection(config schemas.MCPClientConfig) (*clie
ConnectionURL: config.ConnectionString.GetValuePtr(),
}

headers, err := config.HttpHeaders(ctx, m.oauth2Provider)
if err != nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("failed to get HTTP headers: %w", err)
}

// Create StreamableHTTP transport
httpTransport, err := transport.NewStreamableHTTP(config.ConnectionString.GetValue(), transport.WithHTTPHeaders(config.HttpHeaders()))
httpTransport, err := transport.NewStreamableHTTP(config.ConnectionString.GetValue(), transport.WithHTTPHeaders(headers))
if err != nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("failed to create HTTP transport: %w", err)
}
Expand All @@ -586,7 +644,7 @@ func (m *MCPManager) createHTTPConnection(config schemas.MCPClientConfig) (*clie
}

// createSTDIOConnection creates a STDIO-based MCP client connection without holding locks.
func (m *MCPManager) createSTDIOConnection(config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
func (m *MCPManager) createSTDIOConnection(ctx context.Context, config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
if config.StdioConfig == nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("stdio config is required")
}
Expand Down Expand Up @@ -621,7 +679,7 @@ func (m *MCPManager) createSTDIOConnection(config schemas.MCPClientConfig) (*cli
}

// createSSEConnection creates a SSE-based MCP client connection without holding locks.
func (m *MCPManager) createSSEConnection(config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
func (m *MCPManager) createSSEConnection(ctx context.Context, config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
if config.ConnectionString == nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("SSE connection string is required")
}
Expand All @@ -632,8 +690,13 @@ func (m *MCPManager) createSSEConnection(config schemas.MCPClientConfig) (*clien
ConnectionURL: config.ConnectionString.GetValuePtr(), // Reuse HTTPConnectionURL field for SSE URL display
}

headers, err := config.HttpHeaders(ctx, m.oauth2Provider)
if err != nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("failed to get HTTP headers: %w", err)
}

// Create SSE transport
sseTransport, err := transport.NewSSE(config.ConnectionString.GetValue(), transport.WithHeaders(config.HttpHeaders()))
sseTransport, err := transport.NewSSE(config.ConnectionString.GetValue(), transport.WithHeaders(headers))
if err != nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("failed to create SSE transport: %w", err)
}
Expand All @@ -646,7 +709,7 @@ func (m *MCPManager) createSSEConnection(config schemas.MCPClientConfig) (*clien
// createInProcessConnection creates an in-process MCP client connection without holding locks.
// This allows direct connection to an MCP server running in the same process, providing
// the lowest latency and highest performance for tool execution.
func (m *MCPManager) createInProcessConnection(config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
func (m *MCPManager) createInProcessConnection(ctx context.Context, config schemas.MCPClientConfig) (*client.Client, schemas.MCPClientConnectionInfo, error) {
if config.InProcessServer == nil {
return nil, schemas.MCPClientConnectionInfo{}, fmt.Errorf("InProcess connection requires a server instance")
}
Expand Down Expand Up @@ -744,9 +807,10 @@ func (m *MCPManager) createLocalMCPClient() (*schemas.MCPClientState, error) {
ExecutionConfig: schemas.MCPClientConfig{
ID: BifrostMCPClientKey,
Name: BifrostMCPClientKey, // Use same value as ID for consistent prefixing
ToolsToExecute: []string{"*"}, // Allow all tools for internal client
ToolsToExecute: []string{"*"}, // Allow all tools for internal client
},
ToolMap: make(map[string]schemas.ChatTool),
ToolMap: make(map[string]schemas.ChatTool),
ToolNameMapping: make(map[string]string),
ConnectionInfo: schemas.MCPClientConnectionInfo{
Type: schemas.MCPConnectionTypeInProcess, // Accurate: in-process (in-memory) transport
},
Expand Down
19 changes: 10 additions & 9 deletions core/mcp/codemodeexecutecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,10 @@ func (m *ToolsManager) callMCPTool(ctx context.Context, clientName, toolName str
return nil, fmt.Errorf("client not found for server name: %s", clientName)
}

// Strip the client name prefix from tool name before calling MCP server
// The MCP server expects the original tool name, not the prefixed version
originalToolName := stripClientPrefix(toolName, clientName)
// Strip the client name prefix from tool name to get sanitized name
// Then look up the original MCP tool name from the mapping
sanitizedToolName := stripClientPrefix(toolName, clientName)
originalMCPToolName := getOriginalToolName(sanitizedToolName, client)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Code-mode tool calls now bypass sanitization; likely to break tool resolution.

Line 786 onward switches to originalMCPToolName across direct and plugin-backed calls (including Line 913 and Line 974). The MCP system relies on sanitized names for internal resolution; reversing them risks mismatches or call failures. If the server now requires original names, add a fallback path; otherwise keep sanitized names for the call and metadata.

🐛 Proposed fix (primary call path)
- toolNameToCall := originalMCPToolName
+ toolNameToCall := sanitizedToolName

Based on learnings, MCP tool names should remain sanitized throughout the system.

Also applies to: 799-800, 852-853, 859-860, 913-975

🤖 Prompt for AI Agents
In `@core/mcp/codemodeexecutecode.go` around lines 786 - 790, The code replaced
uses of sanitizedToolName with originalMCPToolName, which breaks internal MCP
resolution; revert tool-resolution and metadata usage to sanitizedToolName (the
value produced by stripClientPrefix) throughout the code paths that call tools
(including direct and plugin-backed calls where originalMCPToolName is now
used), and implement a fallback: attempt resolution with sanitizedToolName first
and only query/getOriginalToolName (via getOriginalToolName) and retry with
originalMCPToolName if the sanitized lookup fails. Ensure stripClientPrefix,
sanitizedToolName, getOriginalToolName, and originalMCPToolName are used exactly
as described so metadata and primary call paths keep sanitized names.

// ==================== PLUGIN PIPELINE INTEGRATION ====================
// Set up parent-child request ID tracking and run plugin hooks
Expand All @@ -795,7 +796,7 @@ func (m *ToolsManager) callMCPTool(ctx context.Context, clientName, toolName str
var ok bool
if bifrostCtx, ok = ctx.(*schemas.BifrostContext); !ok {
// Fallback: if not a BifrostContext, execute directly without plugins
return m.callMCPToolDirect(ctx, client, originalToolName, clientName, toolName, args, appendLog)
return m.callMCPToolDirect(ctx, client, originalMCPToolName, clientName, toolName, args, appendLog)
}

originalRequestID, _ := bifrostCtx.Value(schemas.BifrostContextKeyRequestID).(string)
Expand Down Expand Up @@ -848,14 +849,14 @@ func (m *ToolsManager) callMCPTool(ctx context.Context, clientName, toolName str
// Check if plugin pipeline is available
if m.pluginPipelineProvider == nil {
// Fallback: execute directly without plugins
return m.callMCPToolDirect(ctx, client, originalToolName, clientName, toolName, args, appendLog)
return m.callMCPToolDirect(ctx, client, originalMCPToolName, clientName, toolName, args, appendLog)
}

// Get plugin pipeline and run hooks
pipeline := m.pluginPipelineProvider()
if pipeline == nil {
// Fallback: execute directly if pipeline is nil
return m.callMCPToolDirect(ctx, client, originalToolName, clientName, toolName, args, appendLog)
return m.callMCPToolDirect(ctx, client, originalMCPToolName, clientName, toolName, args, appendLog)
}
defer m.releasePluginPipeline(pipeline)

Expand Down Expand Up @@ -909,9 +910,9 @@ func (m *ToolsManager) callMCPTool(ctx context.Context, clientName, toolName str
// Capture start time for latency calculation
startTime := time.Now()

// Derive tool name from originalToolName (ignore pre-hook modifications to tool name)
// Derive tool name from originalMCPToolName (ignore pre-hook modifications to tool name)
// Pre-hooks should not modify which tool gets called, only arguments
toolNameToCall := originalToolName
toolNameToCall := originalMCPToolName

// Call the tool via MCP client
callRequest := mcp.CallToolRequest{
Expand Down Expand Up @@ -970,7 +971,7 @@ func (m *ToolsManager) callMCPTool(ctx context.Context, clientName, toolName str
ChatMessage: createToolResponseMessage(toolCall, rawResult),
ExtraFields: schemas.BifrostMCPResponseExtraFields{
ClientName: clientName,
ToolName: originalToolName,
ToolName: originalMCPToolName,
Latency: latency,
},
}
Expand Down
5 changes: 4 additions & 1 deletion core/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
// both local tool hosting and external MCP server connections.
type MCPManager struct {
ctx context.Context
oauth2Provider schemas.OAuth2Provider // Provider for OAuth2 functionality
toolsManager *ToolsManager // Handler for MCP tools
server *server.MCPServer // Local MCP server instance for hosting tools (STDIO-based)
clientMap map[string]*schemas.MCPClientState // Map of MCP client names to their configurations
Expand All @@ -64,7 +65,7 @@ type MCPToolFunction[T any] func(args T) (string, error)
// Returns:
// - *MCPManager: Initialized manager instance
// - error: Any initialization error
func NewMCPManager(ctx context.Context, config schemas.MCPConfig, logger schemas.Logger) *MCPManager {
func NewMCPManager(ctx context.Context, config schemas.MCPConfig, oauth2Provider schemas.OAuth2Provider, logger schemas.Logger) *MCPManager {
SetLogger(logger)
// Set default values
if config.ToolManagerConfig == nil {
Expand All @@ -78,6 +79,7 @@ func NewMCPManager(ctx context.Context, config schemas.MCPConfig, logger schemas
ctx: ctx,
clientMap: make(map[string]*schemas.MCPClientState),
healthMonitorManager: NewHealthMonitorManager(),
oauth2Provider: oauth2Provider,
}
// Convert plugin pipeline provider functions to the interface expected by ToolsManager
var pluginPipelineProvider func() PluginPipeline
Expand All @@ -98,6 +100,7 @@ func NewMCPManager(ctx context.Context, config schemas.MCPConfig, logger schemas
}

manager.toolsManager = NewToolsManager(config.ToolManagerConfig, manager, config.FetchNewRequestIDFunc, pluginPipelineProvider, releasePluginPipeline)

// Process client configs: create client map entries and establish connections
if len(config.ClientConfigs) > 0 {
// Add clients in parallel
Expand Down
Loading