diff --git a/core/bifrost.go b/core/bifrost.go
index 4c596c900a..6f30cf339e 100644
--- a/core/bifrost.go
+++ b/core/bifrost.go
@@ -3405,7 +3405,7 @@ func (bifrost *Bifrost) GetMCPClients() ([]schemas.MCPClient, error) {
//
// Returns:
// - []schemas.ChatTool: List of available tools
-func (bifrost *Bifrost) GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool {
+func (bifrost *Bifrost) GetAvailableMCPTools(ctx *schemas.BifrostContext) []schemas.ChatTool {
if bifrost.MCPManager == nil {
return nil
}
diff --git a/core/changelog.md b/core/changelog.md
index e69de29bb2..a7419311c6 100644
--- a/core/changelog.md
+++ b/core/changelog.md
@@ -0,0 +1,2 @@
+- feat: add DisableAutoToolInject to MCPToolManagerConfig to suppress automatic MCP tool injection per request
+- feat: add BifrostContextKeyMCPAddedTools to context to track MCP tools added to the request
diff --git a/core/mcp/interface.go b/core/mcp/interface.go
index d573139c07..3779c9b9e7 100644
--- a/core/mcp/interface.go
+++ b/core/mcp/interface.go
@@ -3,8 +3,6 @@
package mcp
import (
- "context"
-
"github.com/maximhq/bifrost/core/schemas"
)
@@ -14,10 +12,10 @@ import (
type MCPManagerInterface interface {
// Tool Operations
// AddToolsToRequest parses available MCP tools and adds them to the request
- AddToolsToRequest(ctx context.Context, req *schemas.BifrostRequest) *schemas.BifrostRequest
+ AddToolsToRequest(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) *schemas.BifrostRequest
// GetAvailableTools returns all available MCP tools for the given context
- GetAvailableTools(ctx context.Context) []schemas.ChatTool
+ GetAvailableTools(ctx *schemas.BifrostContext) []schemas.ChatTool
// ExecuteToolCall executes a single tool call and returns the result
ExecuteToolCall(ctx *schemas.BifrostContext, request *schemas.BifrostMCPRequest) (*schemas.BifrostMCPResponse, error)
diff --git a/core/mcp/mcp.go b/core/mcp/mcp.go
index a38dd99307..a94afa28bb 100644
--- a/core/mcp/mcp.go
+++ b/core/mcp/mcp.go
@@ -163,11 +163,11 @@ func NewMCPManager(ctx context.Context, config schemas.MCPConfig, oauth2Provider
//
// Returns:
// - *schemas.BifrostRequest: The request with tools added
-func (m *MCPManager) AddToolsToRequest(ctx context.Context, req *schemas.BifrostRequest) *schemas.BifrostRequest {
+func (m *MCPManager) AddToolsToRequest(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) *schemas.BifrostRequest {
return m.toolsManager.ParseAndAddToolsToRequest(ctx, req)
}
-func (m *MCPManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool {
+func (m *MCPManager) GetAvailableTools(ctx *schemas.BifrostContext) []schemas.ChatTool {
return m.toolsManager.GetAvailableTools(ctx)
}
diff --git a/core/mcp/toolmanager.go b/core/mcp/toolmanager.go
index 5089880b8f..a37d77d30d 100644
--- a/core/mcp/toolmanager.go
+++ b/core/mcp/toolmanager.go
@@ -177,7 +177,7 @@ func (m *ToolsManager) GetCodeModeDependencies() *CodeModeDependencies {
}
// GetAvailableTools returns the available tools for the given context.
-func (m *ToolsManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool {
+func (m *ToolsManager) GetAvailableTools(ctx *schemas.BifrostContext) []schemas.ChatTool {
availableToolsPerClient := m.clientManager.GetToolPerClient(ctx)
// Flatten tools from all clients into a single slice, avoiding duplicates
var availableTools []schemas.ChatTool
@@ -193,14 +193,14 @@ func (m *ToolsManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool
}
if client.ExecutionConfig.IsCodeModeClient {
includeCodeModeTools = true
- } else {
- // Add tools from this client, checking for duplicates
- for _, tool := range clientTools {
- if tool.Function != nil && tool.Function.Name != "" {
- if !seenToolNames[tool.Function.Name] {
- availableTools = append(availableTools, tool)
- seenToolNames[tool.Function.Name] = true
- }
+ }
+ // Add tools from this client, checking for duplicates
+ for _, tool := range clientTools {
+ if tool.Function != nil && tool.Function.Name != "" && !seenToolNames[tool.Function.Name] {
+ seenToolNames[tool.Function.Name] = true
+ schemas.AppendToContextList(ctx, schemas.BifrostContextKeyMCPAddedTools, tool.Function.Name)
+ if !client.ExecutionConfig.IsCodeModeClient {
+ availableTools = append(availableTools, tool)
}
}
}
@@ -290,7 +290,7 @@ func buildIntegrationDuplicateCheckMap(existingTools []schemas.ChatTool, integra
//
// Returns:
// - *schemas.BifrostRequest: Bifrost request with MCP tools added
-func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schemas.BifrostRequest) *schemas.BifrostRequest {
+func (m *ToolsManager) ParseAndAddToolsToRequest(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) *schemas.BifrostRequest {
// MCP is only supported for chat and responses requests
if req.ChatRequest == nil && req.ResponsesRequest == nil {
return req
diff --git a/core/schemas/bifrost.go b/core/schemas/bifrost.go
index 07f8f33f46..7043484ffb 100644
--- a/core/schemas/bifrost.go
+++ b/core/schemas/bifrost.go
@@ -231,6 +231,7 @@ const (
BifrostContextKeyVideoOutputRequested BifrostContextKey = "bifrost-video-output-requested"
BifrostContextKeyValidateKeys BifrostContextKey = "bifrost-validate-keys" // bool (triggers additional key validation during provider add/update)
BifrostContextKeyProviderResponseHeaders BifrostContextKey = "bifrost-provider-response-headers" // map[string]string (set by provider handlers for response header forwarding)
+ BifrostContextKeyMCPAddedTools BifrostContextKey = "bifrost-mcp-added-tools" // []string (set by bifrost - DO NOT SET THIS MANUALLY)) - list of tools added to the request by MCP, all the tool are in the format "clientName-toolName"
BifrostContextKeyLargePayloadMode BifrostContextKey = "bifrost-large-payload-mode" // bool (set by bifrost - DO NOT SET THIS MANUALLY)) indicates large payload streaming mode is active
BifrostContextKeyLargePayloadReader BifrostContextKey = "bifrost-large-payload-reader" // io.Reader (set by bifrost - DO NOT SET THIS MANUALLY)) upstream reader for large payloads
BifrostContextKeyLargePayloadContentLength BifrostContextKey = "bifrost-large-payload-content-length" // int (set by bifrost - DO NOT SET THIS MANUALLY)) content length for large payloads
diff --git a/docs/features/governance/mcp-tools.mdx b/docs/features/governance/mcp-tools.mdx
index a669f195f2..e881af2f16 100644
--- a/docs/features/governance/mcp-tools.mdx
+++ b/docs/features/governance/mcp-tools.mdx
@@ -20,8 +20,8 @@ The filtering logic is determined by the Virtual Key's configuration:
2. **With MCP Configuration on Virtual Key**
- When you configure MCP clients on a Virtual Key, its settings take full precedence.
- - Bifrost automatically generates an `x-bf-mcp-include-tools` header based on your VK configuration. This acts as a strict allow-list for the request.
- - This generated header **overrides** any `x-bf-mcp-include-tools` header that might have been sent manually with the request.
+ - Bifrost automatically generates an `x-bf-mcp-include-tools` header based on your VK configuration (unless `disable_auto_tool_inject` is enabled or the caller already sent the header). This acts as a strict allow-list for the request.
+ - If the caller already includes an `x-bf-mcp-include-tools` header, auto-injection is skipped — but the VK allow-list is enforced at inference time and still enforced again at MCP tool execution time.
For each MCP client associated with a Virtual Key, you can specify the allowed tools:
- **Select specific tools**: Only the chosen tools from that client will be available.
@@ -40,7 +40,7 @@ You can configure which tools a Virtual Key has access to via the UI.
2. Create/Edit virtual key

3. In **MCP Client Configurations** section, add the MCP client you want to restrict the VK to
-4. Add the tools you want to restrict the VK to, or leave it blank to allow all tools for this client
+4. Select the specific tools to allow, or choose **Allow All Tools** to permit all current and future tools from that client (stored as `*`). Leaving the list empty blocks all tools for that client.
5. Click on the **Save** button
@@ -156,5 +156,5 @@ A request with this Virtual Key cannot access any tools. All tools from all clie
-When a Virtual Key has MCP configurations, it dynamically generates the `x-bf-mcp-include-tools` header. This ensures that the VK's rules are always enforced and will override any manual header sent by the user. Though you can still use `x-bf-mcp-include-clients` header to filter the MCP clients per request.
+When a Virtual Key has MCP configurations, Bifrost enforces the allow-list at both inference time and MCP tool execution time. Auto-injection of the `x-bf-mcp-include-tools` header is skipped if the caller already provides it or if `disable_auto_tool_inject` is enabled — but the VK's restrictions are always applied regardless. You can still use the `x-bf-mcp-include-clients` header to filter MCP clients per request.
\ No newline at end of file
diff --git a/plugins/governance/changelog.md b/plugins/governance/changelog.md
index e69de29bb2..be5706641b 100644
--- a/plugins/governance/changelog.md
+++ b/plugins/governance/changelog.md
@@ -0,0 +1 @@
+- feat: enforce VK MCPConfigs as an execution-time allow-list — empty MCPConfigs denies all MCP tools, non-empty validates each tool in both PreMCPHook and evaluateGovernanceRequest; respects disable_auto_tool_inject toggle (transport config key: mcp_disable_auto_tool_inject) and skips auto-injection header when caller already set it
diff --git a/plugins/governance/main.go b/plugins/governance/main.go
index 65ae1b3820..3b5f312706 100644
--- a/plugins/governance/main.go
+++ b/plugins/governance/main.go
@@ -38,9 +38,10 @@ const (
// Config is the configuration for the governance plugin
type Config struct {
- IsVkMandatory *bool `json:"is_vk_mandatory"`
- RequiredHeaders *[]string `json:"required_headers"` // Pointer to live config slice; changes are reflected immediately without restart
- IsEnterprise bool `json:"is_enterprise"`
+ IsVkMandatory *bool `json:"is_vk_mandatory"`
+ RequiredHeaders *[]string `json:"required_headers"` // Pointer to live config slice; changes are reflected immediately without restart
+ IsEnterprise bool `json:"is_enterprise"`
+ DisableAutoToolInject *bool `json:"disable_auto_tool_inject"`
}
type InMemoryStore interface {
@@ -83,9 +84,10 @@ type GovernancePlugin struct {
cfgMutex sync.RWMutex
- isVkMandatory *bool
- requiredHeaders *[]string // pointer to live config slice; lowercased at check time
- isEnterprise bool
+ isVkMandatory *bool
+ requiredHeaders *[]string // pointer to live config slice; lowercased at check time
+ isEnterprise bool
+ disableAutoToolInject *bool
}
// Init initializes and returns a governance plugin instance.
@@ -150,9 +152,11 @@ func Init(
// Handle nil config - use safe defaults
var isVkMandatory *bool
var requiredHeaders *[]string
+ var disableAutoToolInject *bool
if config != nil {
isVkMandatory = config.IsVkMandatory
requiredHeaders = config.RequiredHeaders
+ disableAutoToolInject = config.DisableAutoToolInject
}
governanceStore, err := NewLocalGovernanceStore(ctx, logger, configStore, governanceConfig, modelCatalog)
@@ -203,21 +207,22 @@ func Init(
ctx, cancelFunc := context.WithCancel(ctx)
plugin := &GovernancePlugin{
- ctx: ctx,
- cancelFunc: cancelFunc,
- store: governanceStore,
- resolver: resolver,
- tracker: tracker,
- engine: engine,
- configStore: configStore,
- modelCatalog: modelCatalog,
- mcpCatalog: mcpCatalog,
- logger: logger,
- isVkMandatory: isVkMandatory,
- cfgMutex: sync.RWMutex{},
- requiredHeaders: requiredHeaders,
- isEnterprise: config != nil && config.IsEnterprise,
- inMemoryStore: inMemoryStore,
+ ctx: ctx,
+ cancelFunc: cancelFunc,
+ store: governanceStore,
+ resolver: resolver,
+ tracker: tracker,
+ engine: engine,
+ configStore: configStore,
+ modelCatalog: modelCatalog,
+ mcpCatalog: mcpCatalog,
+ logger: logger,
+ isVkMandatory: isVkMandatory,
+ cfgMutex: sync.RWMutex{},
+ requiredHeaders: requiredHeaders,
+ isEnterprise: config != nil && config.IsEnterprise,
+ disableAutoToolInject: disableAutoToolInject,
+ inMemoryStore: inMemoryStore,
}
return plugin, nil
}
@@ -259,9 +264,11 @@ func InitFromStore(
// Handle nil config - use safe defaults
var isVkMandatory *bool
var requiredHeaders *[]string
+ var disableAutoToolInject *bool
if config != nil {
isVkMandatory = config.IsVkMandatory
requiredHeaders = config.RequiredHeaders
+ disableAutoToolInject = config.DisableAutoToolInject
}
resolver := NewBudgetResolver(governanceStore, modelCatalog, logger)
tracker := NewUsageTracker(ctx, governanceStore, resolver, configStore, logger)
@@ -288,21 +295,22 @@ func InitFromStore(
}
ctx, cancelFunc := context.WithCancel(ctx)
plugin := &GovernancePlugin{
- ctx: ctx,
- cancelFunc: cancelFunc,
- store: governanceStore,
- resolver: resolver,
- tracker: tracker,
- engine: engine,
- configStore: configStore,
- modelCatalog: modelCatalog,
- mcpCatalog: mcpCatalog,
- logger: logger,
- inMemoryStore: inMemoryStore,
- isVkMandatory: isVkMandatory,
- cfgMutex: sync.RWMutex{},
- requiredHeaders: requiredHeaders,
- isEnterprise: config != nil && config.IsEnterprise,
+ ctx: ctx,
+ cancelFunc: cancelFunc,
+ store: governanceStore,
+ resolver: resolver,
+ tracker: tracker,
+ engine: engine,
+ configStore: configStore,
+ modelCatalog: modelCatalog,
+ mcpCatalog: mcpCatalog,
+ logger: logger,
+ inMemoryStore: inMemoryStore,
+ isVkMandatory: isVkMandatory,
+ cfgMutex: sync.RWMutex{},
+ requiredHeaders: requiredHeaders,
+ isEnterprise: config != nil && config.IsEnterprise,
+ disableAutoToolInject: disableAutoToolInject,
}
return plugin, nil
}
@@ -393,14 +401,27 @@ func (p *GovernancePlugin) HTTPTransportPreHook(ctx *schemas.BifrostContext, req
if err != nil {
return nil, err
}
- //3. Add MCP tools
- headers, err := p.addMCPIncludeTools(nil, virtualKey)
- if err != nil {
- p.logger.Error("failed to add MCP include tools: %v", err)
- return nil, nil
- }
- for header, value := range headers {
- req.Headers[header] = value
+ //3. Add MCP tools only when auto-inject is enabled and header not already set by the caller
+ p.cfgMutex.RLock()
+ autoInjectDisabled := p.disableAutoToolInject != nil && *p.disableAutoToolInject
+ p.cfgMutex.RUnlock()
+ if !autoInjectDisabled {
+ // Treat an explicitly-present (even empty) x-bf-mcp-include-tools header as "present"
+ // so that callers can block auto-injection by sending an empty header value.
+ headerPresent := false
+ for k := range req.Headers {
+ if strings.EqualFold(k, "x-bf-mcp-include-tools") {
+ headerPresent = true
+ break
+ }
+ }
+ if !headerPresent {
+ req.Headers, err = p.addMCPIncludeTools(req.Headers, virtualKey)
+ if err != nil {
+ p.logger.Error("failed to add MCP include tools: %v", err)
+ return nil, nil
+ }
+ }
}
needsMarshal = true
}
@@ -458,14 +479,26 @@ func (p *GovernancePlugin) governLargePayload(ctx *schemas.BifrostContext, req *
if err != nil {
return nil, err
}
- // MCP tool headers — header-only, no body needed
- headers, err := p.addMCPIncludeTools(nil, virtualKey)
- if err != nil {
- p.logger.Error("failed to add MCP include tools: %v", err)
- return nil, nil
- }
- for header, value := range headers {
- req.Headers[header] = value
+ // MCP tool headers — apply the same auto-inject guard as the normal path:
+ // skip when DisableAutoToolInject is set or the caller already sent the header.
+ p.cfgMutex.RLock()
+ autoInjectDisabled := p.disableAutoToolInject != nil && *p.disableAutoToolInject
+ p.cfgMutex.RUnlock()
+ if !autoInjectDisabled {
+ headerPresent := false
+ for k := range req.Headers {
+ if strings.EqualFold(k, "x-bf-mcp-include-tools") {
+ headerPresent = true
+ break
+ }
+ }
+ if !headerPresent {
+ req.Headers, err = p.addMCPIncludeTools(req.Headers, virtualKey)
+ if err != nil {
+ p.logger.Error("failed to add MCP include tools: %v", err)
+ return nil, nil
+ }
+ }
}
}
@@ -986,6 +1019,36 @@ func (p *GovernancePlugin) evaluateGovernanceRequest(ctx *schemas.BifrostContext
}
}
+ // Check the actual MCP tools injected into the request against the VK MCPConfigs.
+ // BifrostContextKeyMCPAddedTools is populated by AddToolsToRequest (which runs before
+ // PreLLMHook), so it contains the real expanded tool names (e.g. "youtube-search") rather
+ // than raw header patterns (e.g. "youtube-*"), giving us exact per-tool validation.
+ if result.Decision == DecisionAllow && result.VirtualKey != nil {
+ if addedTools, ok := ctx.Value(schemas.BifrostContextKeyMCPAddedTools).([]string); ok && len(addedTools) > 0 {
+ if len(result.VirtualKey.MCPConfigs) == 0 {
+ result = &EvaluationResult{
+ Decision: DecisionMCPToolBlocked,
+ Reason: fmt.Sprintf("no MCP tools are configured for virtual key '%s'", result.VirtualKey.Name),
+ VirtualKey: result.VirtualKey,
+ }
+ } else {
+ var disallowed []string
+ for _, tool := range addedTools {
+ if !isMCPToolAllowedByVK(result.VirtualKey, tool) {
+ disallowed = append(disallowed, tool)
+ }
+ }
+ if len(disallowed) > 0 {
+ result = &EvaluationResult{
+ Decision: DecisionMCPToolBlocked,
+ Reason: fmt.Sprintf("MCP tools not allowed for virtual key '%s': %s", result.VirtualKey.Name, strings.Join(disallowed, ", ")),
+ VirtualKey: result.VirtualKey,
+ }
+ }
+ }
+ }
+ }
+
// Mark request as rejected in context if not allowed
if result.Decision != DecisionAllow {
if ctx != nil {
@@ -1027,6 +1090,15 @@ func (p *GovernancePlugin) evaluateGovernanceRequest(ctx *schemas.BifrostContext
},
}
+ case DecisionMCPToolBlocked:
+ return result, &schemas.BifrostError{
+ Type: bifrost.Ptr(string(result.Decision)),
+ StatusCode: bifrost.Ptr(403),
+ Error: &schemas.ErrorField{
+ Message: result.Reason,
+ },
+ }
+
default:
// Fallback to deny for unknown decisions
return result, &schemas.BifrostError{
@@ -1038,6 +1110,36 @@ func (p *GovernancePlugin) evaluateGovernanceRequest(ctx *schemas.BifrostContext
}
}
+// isMCPToolAllowedByVK checks whether a tool pattern (in "clientName-toolName" or "clientName-*"
+// format) is permitted by the virtual key's MCPConfigs.
+//
+// For wildcard patterns ("clientName-*"): allowed if VK has the client configured with any tools.
+// Specific tool enforcement happens at execution time via checkVKMCPToolAllowance.
+// For specific tools ("clientName-toolName"): allowed if VK has "*" or the exact tool name.
+func isMCPToolAllowedByVK(vk *configstoreTables.TableVirtualKey, toolPattern string) bool {
+ for _, mcpConfig := range vk.MCPConfigs {
+ clientName := mcpConfig.MCPClient.Name
+ // Wildcard pattern "clientName-*": VK just needs to have this client configured at all.
+ if toolPattern == clientName+"-*" {
+ if len(mcpConfig.ToolsToExecute) > 0 {
+ return true
+ }
+ continue
+ }
+ // Specific tool "clientName-toolName"
+ if strings.HasPrefix(toolPattern, clientName+"-") {
+ if slices.Contains(mcpConfig.ToolsToExecute, "*") {
+ return true
+ }
+ toolSuffix := strings.TrimPrefix(toolPattern, clientName+"-")
+ if slices.Contains(mcpConfig.ToolsToExecute, toolSuffix) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
// PreLLMHook intercepts requests before they are processed (governance decision point)
// Parameters:
// - ctx: The Bifrost context
@@ -1188,6 +1290,44 @@ func (p *GovernancePlugin) PreMCPHook(ctx *schemas.BifrostContext, req *schemas.
}, nil
}
+ // Blind single-tool check: validate the specific tool being executed against VK MCPConfigs.
+ // This runs independently of evaluateGovernanceRequest to enforce execution-time allow-list.
+ if virtualKeyValue != "" {
+ vk, ok := p.store.GetVirtualKey(virtualKeyValue)
+ if !ok || vk == nil || !vk.IsActive {
+ // VK became invalid after initial check - fail closed for security
+ ctx.SetValue(governanceRejectedContextKey, true)
+ return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
+ Type: bifrost.Ptr(string(DecisionVirtualKeyNotFound)),
+ StatusCode: bifrost.Ptr(403),
+ Error: &schemas.ErrorField{
+ Message: "Virtual key not found",
+ },
+ }}, nil
+ }
+ if len(vk.MCPConfigs) == 0 {
+ ctx.SetValue(governanceRejectedContextKey, true)
+ return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
+ Type: bifrost.Ptr(string(DecisionMCPToolBlocked)),
+ StatusCode: bifrost.Ptr(403),
+ Error: &schemas.ErrorField{
+ Message: fmt.Sprintf("no MCP tools are configured for virtual key '%s'", vk.Name),
+ },
+ }}, nil
+ }
+ if !isMCPToolAllowedByVK(vk, toolName) {
+ ctx.SetValue(governanceRejectedContextKey, true)
+ return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
+ Type: bifrost.Ptr(string(DecisionMCPToolBlocked)),
+ StatusCode: bifrost.Ptr(403),
+ Error: &schemas.ErrorField{
+ Message: fmt.Sprintf("MCP tool '%s' is not allowed for virtual key '%s'", toolName, vk.Name),
+ },
+ }}, nil
+ }
+ return req, nil, nil
+ }
+
return req, nil, nil
}
diff --git a/plugins/governance/resolver.go b/plugins/governance/resolver.go
index fe56192c5b..978e807073 100644
--- a/plugins/governance/resolver.go
+++ b/plugins/governance/resolver.go
@@ -24,6 +24,7 @@ const (
DecisionRequestLimited Decision = "request_limited"
DecisionModelBlocked Decision = "model_blocked"
DecisionProviderBlocked Decision = "provider_blocked"
+ DecisionMCPToolBlocked Decision = "mcp_tool_blocked"
)
// EvaluationRequest contains the context for evaluating a request
diff --git a/transports/bifrost-http/server/plugins.go b/transports/bifrost-http/server/plugins.go
index 157f9f35a5..4effd84fc8 100644
--- a/transports/bifrost-http/server/plugins.go
+++ b/transports/bifrost-http/server/plugins.go
@@ -174,8 +174,9 @@ func (s *BifrostHTTPServer) loadBuiltinPlugins(ctx context.Context) error {
// 3. Governance (if enabled and not enterprise)
if ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
config := &governance.Config{
- IsVkMandatory: &s.Config.ClientConfig.EnforceAuthOnInference,
- RequiredHeaders: &s.Config.ClientConfig.RequiredHeaders,
+ IsVkMandatory: &s.Config.ClientConfig.EnforceAuthOnInference,
+ RequiredHeaders: &s.Config.ClientConfig.RequiredHeaders,
+ DisableAutoToolInject: &s.Config.ClientConfig.MCPDisableAutoToolInject,
}
s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
} else {
diff --git a/transports/bifrost-http/server/server.go b/transports/bifrost-http/server/server.go
index aa256bbdd1..3648712956 100644
--- a/transports/bifrost-http/server/server.go
+++ b/transports/bifrost-http/server/server.go
@@ -242,7 +242,8 @@ func (s *BifrostHTTPServer) ExecuteResponsesMCPTool(ctx context.Context, toolCal
}
func (s *BifrostHTTPServer) GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool {
- return s.Client.GetAvailableMCPTools(ctx)
+ bifrostCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
+ return s.Client.GetAvailableMCPTools(bifrostCtx)
}
// markPluginDisabled marks a plugin as disabled in the plugin status
diff --git a/transports/changelog.md b/transports/changelog.md
index ca15010d82..9a8697d3cb 100644
--- a/transports/changelog.md
+++ b/transports/changelog.md
@@ -1 +1,2 @@
- feat: add option to disable automatic MCP tool injection per request
+- feat: virtual key MCP configs now act as an execution-time allow-list — tools not permitted by the VK are blocked at inference and MCP tool execution
diff --git a/ui/app/workspace/config/views/mcpView.tsx b/ui/app/workspace/config/views/mcpView.tsx
index 5bc0a83a15..3cee876c66 100644
--- a/ui/app/workspace/config/views/mcpView.tsx
+++ b/ui/app/workspace/config/views/mcpView.tsx
@@ -184,7 +184,7 @@ export default function MCPView() {
Disable Auto Tool Injection
- When enabled, MCP tools are not automatically included in every request. Tools are only injected when explicitly specified via request headers (x-bf-mcp-include-tools) or virtual key configuration.
+ When enabled, MCP tools are not automatically included in every request. Tools are only injected when explicitly specified via request headers (x-bf-mcp-include-tools) and still must be allowed by the virtual key MCP configuration.
handleNavigation(item.url, e) : undefined}
>
@@ -289,16 +289,16 @@ const SidebarItemView = ({
return (
(subItem.hasAccess === false ? undefined : handleSubItemClick(subItem, e))}
>
@@ -474,7 +474,7 @@ export default function AppSidebar() {
hasAccess: hasRoutingRulesAccess,
},
{
- title: "Pricing config",
+ title: "Pricing Settings",
url: "/workspace/custom-pricing",
icon: CircleDollarSign,
description: "Pricing configuration",
@@ -687,14 +687,14 @@ export default function AppSidebar() {
},
...(IS_ENTERPRISE
? [
- {
- title: "Proxy",
- url: "/workspace/config/proxy",
- icon: Globe,
- description: "Proxy configuration",
- hasAccess: hasSettingsAccess,
- },
- ]
+ {
+ title: "Proxy",
+ url: "/workspace/config/proxy",
+ icon: Globe,
+ description: "Proxy configuration",
+ hasAccess: hasSettingsAccess,
+ },
+ ]
: []),
{
title: "API Keys",