Overview
Bifrost provides three levels of tool filtering to control which MCP tools are available:
- Client Configuration - Set which tools a client can execute (
tools_to_execute)
- Request Headers - Filter tools per-request via HTTP headers or context
- Virtual Key Configuration - Control tools per-VK (Gateway only)
These levels stack: a tool must pass all applicable filters to be available.
Level 1: Client Configuration
The tools_to_execute field on each MCP client config defines the baseline of available tools.
Semantics
| Value | Behavior |
|---|
["*"] | All tools from this client are available |
[] or omitted | No tools available (deny-by-default) |
["tool1", "tool2"] | Only specified tools are available |
Configuration
Gateway
Go SDK
config.json
curl -X POST http://localhost:8080/api/mcp/client \
-H "Content-Type: application/json" \
-d '{
"name": "filesystem",
"connection_type": "stdio",
"stdio_config": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-filesystem"]
},
"tools_to_execute": ["read_file", "list_directory"]
}'
mcpConfig := &schemas.MCPConfig{
ClientConfigs: []schemas.MCPClientConfig{
{
Name: "filesystem",
ConnectionType: schemas.MCPConnectionTypeSTDIO,
StdioConfig: &schemas.MCPStdioConfig{
Command: "npx",
Args: []string{"-y", "@anthropic/mcp-filesystem"},
},
ToolsToExecute: []string{"read_file", "list_directory"}, // Only these tools
},
},
}
{
"mcp": {
"client_configs": [
{
"name": "filesystem",
"connection_type": "stdio",
"stdio_config": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-filesystem"]
},
"tools_to_execute": ["read_file", "list_directory"]
}
]
}
}
Level 2: Request-Level Filtering
Filter tools dynamically on a per-request basis using headers (Gateway) or context values (SDK).
Available Filters
| Filter | Purpose |
|---|
mcp-include-clients | Only include tools from specified clients |
mcp-include-tools | Only include specified tools (format: clientName-toolName) |
# Include only specific clients
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-clients: filesystem,web_search" \
-d '...'
# Include only specific tools
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-tools: filesystem-read_file,web_search-search" \
-d '...'
# Include all tools from one client, specific tools from another
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-tools: filesystem-*,web_search-search" \
-d '...'
# Include internal tools registered via RegisterTool()
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-tools: bifrostInternal-echo,bifrostInternal-calculator" \
-d '...'
# Empty clients filter blocks ALL tools - no tools available to LLM
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-clients:" \
-d '...'
# Result: No MCP tools available (deny-all)
# Empty tools filter also blocks ALL tools
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-tools:" \
-d '...'
# Result: No MCP tools available (deny-all)
Go SDK Context Values
// Include only specific clients
ctx := context.WithValue(context.Background(),
schemas.BifrostContextKey("mcp-include-clients"),
[]string{"filesystem", "web_search"})
// Include only specific tools
ctx = context.WithValue(ctx,
schemas.BifrostContextKey("mcp-include-tools"),
[]string{"filesystem-read_file", "web_search-search"})
// Wildcard for all tools from a client
ctx = context.WithValue(ctx,
schemas.BifrostContextKey("mcp-include-tools"),
[]string{"filesystem-*", "web_search-search"})
// Include all internal tools (registered via RegisterTool)
ctx = context.WithValue(ctx,
schemas.BifrostContextKey("mcp-include-tools"),
[]string{"bifrostInternal-*"})
response, err := client.ChatCompletionRequest(schemas.NewBifrostContext(ctx, schemas.NoDeadline), request)
// Empty include-clients blocks ALL tools - no tools available
ctx = context.WithValue(context.Background(),
schemas.BifrostContextKey("mcp-include-clients"),
[]string{}) // Empty slice = deny-all
// Result: No MCP tools available to LLM
// Empty include-tools also blocks ALL tools
ctx = context.WithValue(context.Background(),
schemas.BifrostContextKey("mcp-include-tools"),
[]string{}) // Empty slice = deny-all
// Result: No MCP tools available to LLM
Wildcard Support
| Pattern | Meaning |
|---|
* (in include-clients) | Include all clients |
clientName-* (in include-tools) | Include all tools from that client |
clientName-toolName | Include specific tool |
Important: All MCP tools follow a consistent naming convention using the prefixed format clientName-toolName:
-
External MCP Clients (HTTP, SSE, STDIO): Tools use the format
clientName-toolName
- Example:
filesystem-read_file, web_search-search
- The
clientName is the name configured for the MCP client
-
Internal (In-Process) Tools: Tools registered via
RegisterTool() use the prefix bifrostInternal-
- Example:
bifrostInternal-echo, bifrostInternal-my_custom_tool
- These tools are registered via
RegisterTool() in the SDK
This consistent naming convention ensures clear separation between tools from different clients and prevents naming conflicts across all MCP client types.
Level 3: Virtual Key Filtering (Gateway Only)
Virtual Keys can have their own MCP tool access configuration, which takes precedence over request-level headers.
When a Virtual Key has MCP configurations, it generates the x-bf-mcp-include-tools header automatically, overriding any manually sent header.
Configuration
- Navigate to Virtual Keys in the governance section
- Create or edit a Virtual Key
- In MCP Client Configurations, add the clients and tools this VK can access
curl -X POST http://localhost:8080/api/governance/virtual-keys \
-H "Content-Type: application/json" \
-d '{
"name": "support-team-key",
"mcp_configs": [
{
"mcp_client_name": "knowledge_base",
"tools_to_execute": ["search", "get_article"]
},
{
"mcp_client_name": "ticketing",
"tools_to_execute": ["*"]
}
]
}'
{
"governance": {
"virtual_keys": [
{
"name": "support-team-key",
"mcp_configs": [
{
"mcp_client_name": "knowledge_base",
"tools_to_execute": ["search", "get_article"]
},
{
"mcp_client_name": "ticketing",
"tools_to_execute": ["*"]
}
]
}
]
}
}
Virtual Key MCP Config Semantics
| Configuration | Result |
|---|
tools_to_execute: ["*"] | All tools from this client |
tools_to_execute: [] | No tools from this client |
tools_to_execute: ["a", "b"] | Only specified tools |
| Client not configured | All tools blocked from that client |
Learn more in MCP Tool Filtering for Virtual Keys.
Filtering Logic
How Filters Combine
- Client config is the baseline (must include the tool)
- Request filters further narrow down (if specified)
- VK filters override request filters (if VK has MCP configs)
Example Scenario
Setup:
- Client
filesystem has tools_to_execute: ["read_file", "write_file", "delete_file"]
- Virtual Key
prod-key has mcp_configs: [{ mcp_client_name: "filesystem", tools_to_execute: ["read_file"] }]
Request with prod-key:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer vk_prod_key" \
-H "x-bf-mcp-include-tools: filesystem-write_file" \ # This is IGNORED
-d '...'
Result: Only read_file is available (VK config overrides request header)
Request without VK (if allowed):
curl -X POST http://localhost:8080/v1/chat/completions \
-H "x-bf-mcp-include-tools: filesystem-write_file" \
-d '...'
Result: Only write_file is available (request header applies)
Common Patterns
Read-Only Access
Allow only read operations:
{
"tools_to_execute": ["read_file", "list_directory", "get_file_info"]
}
Environment-Based Filtering
Use different VKs for different environments:
{
"virtual_keys": [
{
"name": "development",
"mcp_configs": [
{ "mcp_client_name": "filesystem", "tools_to_execute": ["*"] },
{ "mcp_client_name": "database", "tools_to_execute": ["*"] }
]
},
{
"name": "production",
"mcp_configs": [
{ "mcp_client_name": "filesystem", "tools_to_execute": ["read_file"] },
{ "mcp_client_name": "database", "tools_to_execute": ["query"] }
]
}
]
}
Create VKs for different user roles:
{
"virtual_keys": [
{
"name": "viewer-role",
"mcp_configs": [
{ "mcp_client_name": "documents", "tools_to_execute": ["view", "search"] }
]
},
{
"name": "editor-role",
"mcp_configs": [
{ "mcp_client_name": "documents", "tools_to_execute": ["view", "search", "edit", "create"] }
]
},
{
"name": "admin-role",
"mcp_configs": [
{ "mcp_client_name": "documents", "tools_to_execute": ["*"] }
]
}
]
}
Advanced: Context-Based Filtering
For SDK users, filtering can be applied at the context level, enabling per-request tool customization:
Go SDK Context Filtering
import (
"context"
"github.com/maximhq/bifrost/core/schemas"
)
// Filter to specific clients
ctx := context.WithValue(
context.Background(),
schemas.BifrostContextKey("mcp-include-clients"),
[]string{"filesystem", "web_search"},
)
// Or filter to specific tools
ctx = context.WithValue(
ctx,
schemas.BifrostContextKey("mcp-include-tools"),
[]string{"filesystem-read_file", "web_search-search"},
)
// Request will only see filtered tools
response, _ := client.ChatCompletionRequest(schemas.NewBifrostContext(ctx, schemas.NoDeadline), request)
Filter Precedence
When multiple filters apply, they combine as an intersection (AND logic):
Client Config Tools ∩ Request Filters ∩ VK Filters = Available Tools
Example:
- Client config allows: [read_file, write_file, delete_file]
- Request header specifies: [read_file, write_file]
- VK config restricts to: [read_file]
- Result: Only [read_file] available
Gateway API:
curl http://localhost:8080/api/mcp/clients
Response shows tools per client:
[
{
"config": { "name": "filesystem", "tools_to_execute": ["read_file", "write_file"] },
"tools": [
{ "name": "read_file", "description": "Read file contents" },
{ "name": "write_file", "description": "Write to file" }
],
"state": "connected"
}
]
Check What LLM Receives
The tools included in a chat request depend on all active filters. To see what tools are available for a specific request, check the request body sent to the LLM provider in your logs or observability platform.
Next Steps