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
1 change: 1 addition & 0 deletions core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/bytedance/sonic v1.15.0
github.com/fasthttp/websocket v1.5.12
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
github.com/hajimehoshi/go-mp3 v0.3.4
github.com/klauspost/compress v1.18.2
github.com/mark3labs/mcp-go v0.43.2
Expand Down
16 changes: 8 additions & 8 deletions core/internal/mcptests/agent_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ func SetupAgentTest(t *testing.T, config AgentTestConfig) (*mcp.MCPManager, *Dyn

// Create context with filtering
baseCtx := context.Background()
if len(config.ClientFiltering) > 0 {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, config.ClientFiltering)
if config.ClientFiltering != nil {
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, config.ClientFiltering)
}
if len(config.ToolFiltering) > 0 {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, config.ToolFiltering)
if config.ToolFiltering != nil {
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, config.ToolFiltering)
}
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -192,11 +192,11 @@ func SetupAgentTestWithClients(t *testing.T, config AgentTestConfig, customClien

// Create context with filtering
baseCtx := context.Background()
if len(config.ClientFiltering) > 0 {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, config.ClientFiltering)
if config.ClientFiltering != nil {
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, config.ClientFiltering)
}
if len(config.ToolFiltering) > 0 {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, config.ToolFiltering)
if config.ToolFiltering != nil {
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, config.ToolFiltering)
}
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)

Expand Down
40 changes: 20 additions & 20 deletions core/internal/mcptests/codemode_stdio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,27 @@ func setupCodeModeWithSTDIOServers(t *testing.T, serverNames ...string) (*mcp.MC
config = GetTemperatureMCPClientConfig(bifrostRoot)
config.IsCodeModeClient = true
config.ID = "temperature-client" // Match test expectations
config.Name = "temperature" // Use lowercase to match test code
config.Name = "temperature" // Use lowercase to match test code
config.ToolsToAutoExecute = []string{"executeToolCode", "listToolFiles", "readToolFile"}
case "go-test-server":
config = GetGoTestServerConfig(bifrostRoot)
config.ID = "goTestServer-client" // Match test expectations
config.Name = "goTestServer" // Use camelCase to match test code
config.Name = "goTestServer" // Use camelCase to match test code
config.ToolsToAutoExecute = []string{"executeToolCode", "listToolFiles", "readToolFile"}
case "edge-case-server":
config = GetEdgeCaseServerConfig(bifrostRoot)
config.ID = "edgeCaseServer-client" // Match test expectations
config.Name = "edgeCaseServer" // Use camelCase to match test code
config.Name = "edgeCaseServer" // Use camelCase to match test code
config.ToolsToAutoExecute = []string{"executeToolCode", "listToolFiles", "readToolFile"}
case "error-test-server":
config = GetErrorTestServerConfig(bifrostRoot)
config.ID = "errorTestServer-client" // Match test expectations
config.Name = "errorTestServer" // Use camelCase to match test code
config.Name = "errorTestServer" // Use camelCase to match test code
config.ToolsToAutoExecute = []string{"executeToolCode", "listToolFiles", "readToolFile"}
case "parallel-test-server":
config = GetParallelTestServerConfig(bifrostRoot)
config.ID = "parallelTestServer-client" // Match test expectations
config.Name = "parallelTestServer" // Use camelCase to match test code
config.Name = "parallelTestServer" // Use camelCase to match test code
config.ToolsToAutoExecute = []string{"executeToolCode", "listToolFiles", "readToolFile"}
case "test-tools-server":
// test-tools-server doesn't have a fixture, set up manually
Expand Down Expand Up @@ -367,32 +367,32 @@ func TestCodeMode_STDIO_ServerFiltering(t *testing.T) {
expectedError string
}{
{
name: "allow_only_test_tools_server",
includeClients: []string{"testToolsServer"},
code: `result = testToolsServer.echo(message="allowed")`,
name: "allow_only_test_tools_server",
includeClients: []string{"testToolsServer"},
code: `result = testToolsServer.echo(message="allowed")`,
shouldSucceed: true,
expectedInResult: "allowed",
},
{
name: "block_test_tools_server",
includeClients: []string{"temperature"},
code: `result = testToolsServer.echo(message="blocked")`,
shouldSucceed: false,
expectedError: "undefined: testToolsServer",
shouldSucceed: false,
expectedError: "undefined: testToolsServer",
},
{
name: "allow_only_temperature_server",
includeClients: []string{"temperature"},
code: `result = temperature.get_temperature(location="Paris")`,
name: "allow_only_temperature_server",
includeClients: []string{"temperature"},
code: `result = temperature.get_temperature(location="Paris")`,
shouldSucceed: true,
expectedInResult: "Paris",
},
{
name: "block_temperature_server",
includeClients: []string{"testToolsServer"},
code: `result = temperature.get_temperature(location="blocked")`,
shouldSucceed: false,
expectedError: "undefined: temperature",
shouldSucceed: false,
expectedError: "undefined: temperature",
},
{
name: "allow_both_servers",
Expand All @@ -409,7 +409,7 @@ result = {"echo": echo, "temp": temp}`,
t.Run(tc.name, func(t *testing.T) {
// Create context with client filtering
baseCtx := context.Background()
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, tc.includeClients)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, tc.includeClients)
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)

// Verify filtering is applied at tool listing level
Expand Down Expand Up @@ -524,7 +524,7 @@ result = {"echo": echo, "calc": calc}`,
t.Run(tc.name, func(t *testing.T) {
// Create context with tool filtering
baseCtx := context.Background()
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, tc.includeTools)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, tc.includeTools)
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)

// Verify filtering is applied
Expand Down Expand Up @@ -622,10 +622,10 @@ result = {"echo": echo, "temp": temp}`,
// Create context with both client and tool filtering
baseCtx := context.Background()
if tc.includeClients != nil {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, tc.includeClients)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, tc.includeClients)
}
if tc.includeTools != nil {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, tc.includeTools)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, tc.includeTools)
}
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)

Expand Down Expand Up @@ -1692,7 +1692,7 @@ result = {"count": 3}`,
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
baseCtx := context.Background()
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, tc.includeClients)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, tc.includeClients)
ctx := schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)

toolCall := CreateExecuteToolCodeCall(fmt.Sprintf("call-%s", tc.name), tc.code)
Expand Down
9 changes: 4 additions & 5 deletions core/internal/mcptests/concurrency_advanced_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"testing"
"time"

"github.com/maximhq/bifrost/core/mcp"
"github.com/maximhq/bifrost/core/schemas"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -533,14 +532,14 @@ func TestConcurrent_FilteringChanges(t *testing.T) {
if id%2 == 0 {
// Even: allow all tools
baseCtx := context.Background()
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, []string{"*"})
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, []string{"bifrostInternal-*"})
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, []string{"*"})
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, []string{"bifrostInternal-*"})
ctx = schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)
} else {
// Odd: allow only echo
baseCtx := context.Background()
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, []string{"*"})
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, []string{"bifrostInternal-echo"})
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, []string{"*"})
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, []string{"bifrostInternal-echo"})
ctx = schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)
}

Expand Down
4 changes: 2 additions & 2 deletions core/internal/mcptests/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -1984,10 +1984,10 @@ func AssertExecutionTimeUnder(t *testing.T, fn func(), maxDuration time.Duration
func CreateTestContextWithMCPFilter(includeClients []string, includeTools []string) *schemas.BifrostContext {
baseCtx := context.Background()
if includeClients != nil {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeClients, includeClients)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeClients, includeClients)
}
if includeTools != nil {
baseCtx = context.WithValue(baseCtx, mcp.MCPContextKeyIncludeTools, includeTools)
baseCtx = context.WithValue(baseCtx, schemas.MCPContextKeyIncludeTools, includeTools)
}
return schemas.NewBifrostContext(baseCtx, schemas.NoDeadline)
}
Expand Down
7 changes: 0 additions & 7 deletions core/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ const (
BifrostMCPClientKey = "bifrostInternal" // Key for internal Bifrost client in clientMap
MCPLogPrefix = "[Bifrost MCP]" // Consistent logging prefix
MCPClientConnectionEstablishTimeout = 30 * time.Second // Timeout for MCP client connection establishment

// Context keys for client filtering in requests
// NOTE: []string is used for both keys, and by default all clients/tools are included (when nil).
// If "*" is present, all clients/tools are included, and [] means no clients/tools are included.
// Request context filtering takes priority over client config - context can override client exclusions.
MCPContextKeyIncludeClients schemas.BifrostContextKey = "mcp-include-clients" // Context key for whitelist client filtering
MCPContextKeyIncludeTools schemas.BifrostContextKey = "mcp-include-tools" // Context key for whitelist tool filtering (Note: toolName should be in "clientName-toolName" format for individual tools, or "clientName-*" for wildcard)
)

// ============================================================================
Expand Down
5 changes: 3 additions & 2 deletions core/mcp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (m *MCPManager) GetToolPerClient(ctx context.Context) map[string][]schemas.
var includeClients []string

// Extract client filtering from request context
if existingIncludeClients, ok := ctx.Value(MCPContextKeyIncludeClients).([]string); ok && existingIncludeClients != nil {
if existingIncludeClients, ok := ctx.Value(schemas.MCPContextKeyIncludeClients).([]string); ok && existingIncludeClients != nil {
includeClients = existingIncludeClients
}

Expand Down Expand Up @@ -439,7 +439,7 @@ func canAutoExecuteTool(toolName string, config *schemas.MCPClientConfig) bool {
// Context filtering can only NARROW the tools available, NOT expand beyond client configuration.
// This is checked AFTER client-level filtering (shouldSkipToolForConfig).
func shouldSkipToolForRequest(ctx context.Context, clientName, toolName string) bool {
includeTools := ctx.Value(MCPContextKeyIncludeTools)
includeTools := ctx.Value(schemas.MCPContextKeyIncludeTools)

if includeTools != nil {
// Try []string first (preferred type)
Expand Down Expand Up @@ -752,6 +752,7 @@ func hasToolCallsForChatResponse(response *schemas.BifrostChatResponse) bool {
if choice.FinishReason != nil && *choice.FinishReason == "tool_calls" {
return true
}

// Check if message has tool calls regardless of finish_reason.
// Some providers (e.g. Gemini) return finish_reason "stop" even when tool calls are present,
// so we cannot rely solely on finish_reason to detect tool calls.
Expand Down
21 changes: 14 additions & 7 deletions core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,20 @@ type BifrostContextKey string

// BifrostContextKeyRequestType is a context key for the request type.
const (
BifrostContextKeySessionToken BifrostContextKey = "bifrost-session-token" // string (session token for authentication - set by auth middleware)
BifrostContextKeyVirtualKey BifrostContextKey = "x-bf-vk" // string
BifrostContextKeyAPIKeyName BifrostContextKey = "x-bf-api-key" // string (explicit key name selection)
BifrostContextKeyAPIKeyID BifrostContextKey = "x-bf-api-key-id" // string (explicit key ID selection, takes priority over name)
BifrostContextKeyRequestID BifrostContextKey = "request-id" // string
BifrostContextKeyFallbackRequestID BifrostContextKey = "fallback-request-id" // string
BifrostContextKeyDirectKey BifrostContextKey = "bifrost-direct-key" // Key struct
BifrostContextKeySessionToken BifrostContextKey = "bifrost-session-token" // string (session token for authentication - set by auth middleware)
BifrostContextKeyVirtualKey BifrostContextKey = "x-bf-vk" // string
BifrostContextKeyAPIKeyName BifrostContextKey = "x-bf-api-key" // string (explicit key name selection)
BifrostContextKeyAPIKeyID BifrostContextKey = "x-bf-api-key-id" // string (explicit key ID selection, takes priority over name)
BifrostContextKeyRequestID BifrostContextKey = "request-id" // string
BifrostContextKeyFallbackRequestID BifrostContextKey = "fallback-request-id" // string
BifrostContextKeyDirectKey BifrostContextKey = "bifrost-direct-key" // Key struct

// NOTE: []string is used for both keys, and by default all clients/tools are included (when nil).
// If "*" is present, all clients/tools are included, and [] means no clients/tools are included.
// Request context filtering takes priority over client config - context can override client exclusions.
MCPContextKeyIncludeClients BifrostContextKey = "mcp-include-clients" // Context key for whitelist client filtering
MCPContextKeyIncludeTools BifrostContextKey = "mcp-include-tools" // Context key for whitelist tool filtering (Note: toolName should be in "clientName-toolName" format for individual tools, or "clientName-*" for wildcard)

BifrostContextKeySelectedKeyID BifrostContextKey = "bifrost-selected-key-id" // string (to store the selected key ID (set by bifrost governance plugin - DO NOT SET THIS MANUALLY))
BifrostContextKeySelectedKeyName BifrostContextKey = "bifrost-selected-key-name" // string (to store the selected key name (set by bifrost governance plugin - DO NOT SET THIS MANUALLY))
BifrostContextKeyGovernanceVirtualKeyID BifrostContextKey = "bifrost-governance-virtual-key-id" // string (to store the virtual key ID (set by bifrost governance plugin - DO NOT SET THIS MANUALLY))
Expand Down
4 changes: 2 additions & 2 deletions docs/features/governance/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Make sure you have at least one MCP client set up. Read more about it [here](../
The filtering logic is determined by the Virtual Key's configuration:

1. **No MCP Configuration on Virtual Key (Default)**
- If a Virtual Key has no specific MCP configurations, all tools from all enabled MCP clients are available by default.
- In this state, a user can still manually filter tools for a single request by passing the `x-bf-mcp-include-tools` header.
- If a Virtual Key has no specific MCP configurations, **no MCP tools are available** (deny-by-default).
- You must explicitly add MCP client configurations to allow tools.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

2. **With MCP Configuration on Virtual Key**
- When you configure MCP clients on a Virtual Key, its settings take full precedence.
Expand Down
4 changes: 2 additions & 2 deletions docs/features/governance/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ This powerful feature enables key use cases like:
Virtual Keys can be restricted to use only specific provider/models. When provider/model restrictions are configured, the VK can only access those designated provider/models, providing fine-grained control over which provider/models different users or applications can utilize.

**How It Works:**
- **No Restrictions** (default): VK can use any available provider/models based on global configuration
- **With Restrictions**: VK limited to only the specified provider/models with weighted load balancing
- **No Provider Configs** (default): VK **blocks all providers** (deny-by-default). You must add provider configurations to allow traffic.
- **With Provider Configs**: VK limited to only the specified provider/models. Configured providers participate in weighted load balancing only if their `weight` is set to a numeric value, while providers with `weight: null` remain configured but are opted out of weighted selection.

**Model Validation:**
When you configure provider restrictions on a Virtual Key, Bifrost validates that the requested model is allowed for the selected provider:
Expand Down
2 changes: 1 addition & 1 deletion docs/mcp/filtering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ This consistent naming convention ensures clear separation between tools from di
Virtual Keys can have their own MCP tool access configuration, which **takes precedence** over request-level headers.

<Note>
When a Virtual Key has MCP configurations, it generates the `x-bf-mcp-include-tools` header automatically, overriding any manually sent header.
When a Virtual Key has no MCP configurations, **no MCP tools are available** (deny-by-default). You must explicitly add MCP client configurations to allow tools. When a Virtual Key has MCP configurations, it generates the `x-bf-mcp-include-tools` header automatically, overriding any manually sent header.
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
</Note>

### Configuration
Expand Down
8 changes: 8 additions & 0 deletions docs/openapi/schemas/management/governance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ VirtualKeyProviderConfig:
type: string
weight:
type: number
nullable: true
description: Weight for provider load balancing. Null means excluded from weighted routing.
allowed_models:
type: array
items:
Expand Down Expand Up @@ -195,13 +197,16 @@ CreateVirtualKeyRequest:
type: string
provider_configs:
type: array
description: Provider configurations (empty means no providers allowed, deny-by-default)
items:
type: object
properties:
provider:
type: string
weight:
type: number
nullable: true
description: Weight for load balancing. Null means excluded from weighted routing.
allowed_models:
type: array
items:
Expand All @@ -216,6 +221,7 @@ CreateVirtualKeyRequest:
type: string
mcp_configs:
type: array
description: MCP configurations (empty means no MCP tools allowed, deny-by-default)
items:
type: object
properties:
Expand Down Expand Up @@ -255,6 +261,8 @@ UpdateVirtualKeyRequest:
type: string
weight:
type: number
nullable: true
description: Weight for load balancing. Null means excluded from weighted routing.
allowed_models:
type: array
items:
Expand Down
Loading
Loading