fix: deterministic MCP tool ordering for prompt cache stability - #4588
Conversation
📝 WalkthroughWalkthroughFixes non-deterministic MCP tool ordering by sorting client names and tool names before iteration in ChangesDeterministic MCP Tool Ordering
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Sort client and tool names lexicographically in GetToolPerClient and GetAvailableTools so the tool list injected into LLM prompts is stable across requests. Also sort keys in OrderedMapFromMap so tool schema properties don't shuffle. Closes maximhq#2347
922919a to
5d6e92d
Compare
Confidence Score: 3/5Safe to merge for the common case (stable MCP connections), but the property-ordering gap means cache misses can still occur after any client reconnection even when tools haven't changed. The client and tool list sorting in utils.go and toolmanager.go correctly eliminates the request-to-request ordering variance. However, convertMCPToolToBifrostSchema still builds property OrderedMaps by iterating a plain Go map without sorting, so tool schema property keys remain non-deterministic across reconnection events. The orderedmap.go fix does not reach this code path. Additionally, the clientsByName construction silently drops one client's tools whenever two clients share the same name, which is a behavior change from the original loop. core/mcp/utils.go — the convertMCPToolToBifrostSchema property/defs building loops (lines ~524 and ~548) and the new clientsByName construction block (lines 75–81). Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Req as Incoming Request
participant TM as ToolsManager
participant MM as MCPManager
participant CM as clientMap (map[ID]→State)
Req->>TM: GetAvailableTools(ctx)
TM->>MM: GetToolPerClient(ctx)
MM->>CM: iterate clientMap
Note over MM: collect names → clientsByName<br/>slices.Sort(clientNames) ✅
loop sorted clientNames
MM->>MM: iterate sorted toolNames ✅
MM-->>MM: tools[clientName] append
end
MM-->>TM: map[clientName][]ChatTool
Note over TM: slices.Sort(sortedClients) ✅
loop sorted clients
TM-->>TM: flatten into availableTools
end
TM-->>Req: []ChatTool (deterministic order ✅)
Note over MM: At tool-discovery time (reconnect)
MM->>MM: convertMCPToolToBifrostSchema
Note over MM: for k,v := range Properties (Go map)<br/>→ NewOrderedMapWithCapacity+Set ❌<br/>key order non-deterministic
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Req as Incoming Request
participant TM as ToolsManager
participant MM as MCPManager
participant CM as clientMap (map[ID]→State)
Req->>TM: GetAvailableTools(ctx)
TM->>MM: GetToolPerClient(ctx)
MM->>CM: iterate clientMap
Note over MM: collect names → clientsByName<br/>slices.Sort(clientNames) ✅
loop sorted clientNames
MM->>MM: iterate sorted toolNames ✅
MM-->>MM: tools[clientName] append
end
MM-->>TM: map[clientName][]ChatTool
Note over TM: slices.Sort(sortedClients) ✅
loop sorted clients
TM-->>TM: flatten into availableTools
end
TM-->>Req: []ChatTool (deterministic order ✅)
Note over MM: At tool-discovery time (reconnect)
MM->>MM: convertMCPToolToBifrostSchema
Note over MM: for k,v := range Properties (Go map)<br/>→ NewOrderedMapWithCapacity+Set ❌<br/>key order non-deterministic
|
|
Good catch on Re: duplicate client names — |
* origin/dev: (76 commits) fix: deterministic MCP tool ordering for prompt cache stability (maximhq#4588) enterprise changelog (maximhq#4586) Adds changelog for v1.5.16 --skip-ci transports: update dependencies --skip-ci fixes go worspace setup for cost-accuracy and load-test (maximhq#4585) plugins/telemetry: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/semanticcache: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/prompts: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/otel: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/modelcatalogresolver: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/mocker: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/maxim: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/logging: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/jsonparser: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/governance: bump core to v1.5.22 and framework to v1.3.22 --skip-ci plugins/compat: bump core to v1.5.22 and framework to v1.3.22 --skip-ci framework: bump core to v1.5.22 --skip-ci build fix (maximhq#4584) build fix chore: regenerate openapi.json --skip-ci ... # Conflicts: # .github/workflows/scripts/cost-accuracy-test.sh # core/changelog.md # tests/config.json # tests/integrations/python/config.json
Sort client and tool names lexicographically in GetToolPerClient and GetAvailableTools so the tool list injected into LLM prompts is stable across requests. Also sort keys in OrderedMapFromMap so tool schema properties don't shuffle. Closes #2347
…mhq#4588) Sort client and tool names lexicographically in GetToolPerClient and GetAvailableTools so the tool list injected into LLM prompts is stable across requests. Also sort keys in OrderedMapFromMap so tool schema properties don't shuffle. Closes maximhq#2347
…mhq#4588) Sort client and tool names lexicographically in GetToolPerClient and GetAvailableTools so the tool list injected into LLM prompts is stable across requests. Also sort keys in OrderedMapFromMap so tool schema properties don't shuffle. Closes maximhq#2347
convertMCPToolToBifrostSchema built the OrderedMap for inputSchema properties and $defs by ranging over the Go maps mcp-go decodes them into. Map iteration order is random, and the conversion runs again on every connection check tick, so the stored tool could change property order at every sync. The tool JSON sent to providers changed with it, which invalidated provider prompt caches. computeToolsHash changed too, so the callback set with SetToolsChangeCallback fired for tools that had not changed. Copy both maps with OrderedMapFromMap, which sorts keys. mcp-go does not record the server's key order, so sorted order is the stable order available here. maximhq#2348 proposed this change and was closed after maximhq#4588 merged, but maximhq#4588 sorted tool and client names and did not change this function. Related: maximhq#3362, maximhq#6591. Fixes maximhq#7169 Affected packages: - core/mcp/utils.go - core/mcp/toolschemaorder_test.go - core/changelog.md
Summary
MCP tools are assembled from Go maps with undefined iteration order, causing the tool list to shuffle across requests. This breaks prefix-based prompt caching (~11x cost multiplier).
Changes
core/mcp/utils.go— Sort client names and tool names before iteration inGetToolPerClientcore/mcp/toolmanager.go— Sort client names inGetAvailableToolsbefore flatteningcore/schemas/orderedmap.go— Sort keys inOrderedMapFromMapso tool schema properties are deterministicType of change
Affected areas
Breaking changes
Related issues
Closes #2347
Checklist
docs/contributing/README.mdand followed the guidelines