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 ![Virtual Key MCP Tool Restrictions](../../media/ui-virtual-key-mcp-filter.png) 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",