Skip to content
Open
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/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix: deterministic MCP tool ordering for prompt cache stability (closes #2347)
11 changes: 10 additions & 1 deletion core/mcp/toolmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"sync/atomic"
"time"
Expand Down Expand Up @@ -214,7 +215,15 @@ func (m *ToolsManager) GetAvailableTools(ctx *schemas.BifrostContext) []schemas.
// Track tool names to prevent duplicates
seenToolNames := make(map[string]bool)

for clientName, clientTools := range availableToolsPerClient {
// Sort client names for deterministic tool ordering
sortedClients := make([]string, 0, len(availableToolsPerClient))
for clientName := range availableToolsPerClient {
sortedClients = append(sortedClients, clientName)
}
slices.Sort(sortedClients)

for _, clientName := range sortedClients {
clientTools := availableToolsPerClient[clientName]
client := m.clientManager.GetClientByName(clientName)
if client == nil {
m.logger.Warn("%s Client %s not found, skipping", MCPLogPrefix, clientName)
Expand Down
26 changes: 21 additions & 5 deletions core/mcp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,19 @@ func (m *MCPManager) GetToolPerClient(ctx context.Context) map[string][]schemas.

m.logger.Debug("%s GetToolPerClient: Total clients in manager: %d, Filter: %v", MCPLogPrefix, len(m.clientMap), includeClients)

tools := make(map[string][]schemas.ChatTool)
// Collect and sort client names for deterministic tool ordering
clientNames := make([]string, 0, len(m.clientMap))
clientsByName := make(map[string]*schemas.MCPClientState, len(m.clientMap))
for _, client := range m.clientMap {
// Use client name as the key (not ID)
clientName := client.ExecutionConfig.Name
name := client.ExecutionConfig.Name
clientNames = append(clientNames, name)
clientsByName[name] = client
}
slices.Sort(clientNames)

tools := make(map[string][]schemas.ChatTool)
for _, clientName := range clientNames {
client := clientsByName[clientName]
clientID := client.ExecutionConfig.ID

m.logger.Debug("%s Evaluating client %s (ID: %s) for tools", MCPLogPrefix, clientName, clientID)
Expand All @@ -91,12 +100,19 @@ func (m *MCPManager) GetToolPerClient(ctx context.Context) map[string][]schemas.
continue
}

// Add all tools from this client
// Collect and sort tool names for deterministic ordering
toolNames := make([]string, 0, len(client.ToolMap))
for toolName := range client.ToolMap {
toolNames = append(toolNames, toolName)
}
slices.Sort(toolNames)

// FILTERING HIERARCHY (restrictive, not permissive):
// 1. Client-level configuration (ToolsToExecute) - Global allow-list, most restrictive
// 2. Request context (MCPContextKeyIncludeTools) - Can only further narrow, not expand
// Context filtering CANNOT override client configuration - it can only be more restrictive.
for toolName, tool := range client.ToolMap {
for _, toolName := range toolNames {
tool := client.ToolMap[toolName]
// First check: Client configuration is the global allow-list
// If client config blocks a tool, it CANNOT be overridden by context
if shouldSkipToolForConfig(toolName, client.ExecutionConfig) {
Expand Down
4 changes: 2 additions & 2 deletions core/schemas/orderedmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ func NewOrderedMapFromPairs(pairs ...Pair) *OrderedMap {
}

// OrderedMapFromMap creates an OrderedMap from a plain map.
// Key order is NOT guaranteed since Go maps have undefined iteration order.
// Use this only when insertion order doesn't matter (e.g., for hashing).
// Keys are sorted lexicographically to ensure deterministic ordering.
func OrderedMapFromMap(m map[string]interface{}) *OrderedMap {
if m == nil {
return nil
Expand All @@ -68,6 +67,7 @@ func OrderedMapFromMap(m map[string]interface{}) *OrderedMap {
om.keys = append(om.keys, k)
om.values[k] = v
}
sort.Strings(om.keys)
return om
}

Expand Down

This file was deleted.

This file was deleted.

3 changes: 1 addition & 2 deletions ui/app/_fallbacks/enterprise/lib/contexts/rbacContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export enum RbacResource {
RBAC = "RBAC",
Governance = "Governance",
RoutingRules = "RoutingRules",
PIIRedactor = "PIIRedactor",
PromptRepository = "PromptRepository",
PromptDeploymentStrategy = "PromptDeploymentStrategy",
SkillsRepository = "SkillsRepository",
Expand Down Expand Up @@ -92,4 +91,4 @@ export function useRbacContext() {
};
}
return context;
}
}
17 changes: 0 additions & 17 deletions ui/app/workspace/pii-redactor/layout.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions ui/app/workspace/pii-redactor/page.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions ui/app/workspace/pii-redactor/providers/layout.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions ui/app/workspace/pii-redactor/providers/page.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions ui/app/workspace/pii-redactor/rules/layout.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions ui/app/workspace/pii-redactor/rules/page.tsx

This file was deleted.

Loading