Skip to content

Commit e5da24c

Browse files
fix(server): sort default toolset alphabetically for stable ordering (#3539)
## Summary This PR resolves issue #3463 by sorting the tools in the default toolset alphabetically during server initialization. Currently, the default toolset (`cfg.ToolsetConfigs[""]`) is populated by iterating over the `toolsMap`. Because map iteration order in Go is randomized, tools were being returned in an arbitrary order across server restarts. This broke client-side optimizations like Anthropic Prompt Caching that rely on a stable tool order. By sorting `allToolNames` with `slices.Sort()` prior to populating the default toolset, we guarantee stable alphabetical ordering while leaving custom toolsets (which are manually ordered in user configs) untouched. ## Testing - Added unit test `TestDefaultToolsetIsAlphabeticallySorted` in `internal/server/server_test.go` to explicitly verify that out-of-order tools get sorted correctly in the default toolset. - Verified that all existing server unit tests pass. Fixes #3463 --------- Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
1 parent 7292613 commit e5da24c

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

‎internal/server/server.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ func initializeToolsets(ctx context.Context, cfg ServerConfig, toolsMap map[stri
335335
for name := range toolsMap {
336336
allToolNames = append(allToolNames, name)
337337
}
338+
slices.Sort(allToolNames)
338339
if cfg.ToolsetConfigs == nil {
339340
cfg.ToolsetConfigs = make(ToolsetConfigs)
340341
}

‎internal/server/server_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,3 +1449,39 @@ func TestMCPAuthEnableAPIClash(t *testing.T) {
14491449
t.Errorf("unexpected error message: %v", err)
14501450
}
14511451
}
1452+
1453+
func TestDefaultToolsetIsAlphabeticallySorted(t *testing.T) {
1454+
ctx, err := testutils.ContextWithNewLogger()
1455+
if err != nil {
1456+
t.Fatalf("error setting up logger: %s", err)
1457+
}
1458+
instrumentation, err := telemetry.CreateTelemetryInstrumentation("0.0.0")
1459+
if err != nil {
1460+
t.Fatalf("unexpected error: %s", err)
1461+
}
1462+
ctx = util.WithInstrumentation(ctx, instrumentation)
1463+
1464+
cfg := server.ServerConfig{
1465+
Version: "0.0.0",
1466+
ToolConfigs: server.ToolConfigs{
1467+
"zoo": offlineToolConfig{name: "zoo"},
1468+
"apple": offlineToolConfig{name: "apple"},
1469+
"banana": offlineToolConfig{name: "banana"},
1470+
},
1471+
}
1472+
1473+
_, toolsetsMap, err := server.InitializeOfflineConfigs(ctx, cfg)
1474+
if err != nil {
1475+
t.Fatalf("InitializeOfflineConfigs returned error: %s", err)
1476+
}
1477+
1478+
defaultToolset, ok := toolsetsMap[""]
1479+
if !ok {
1480+
t.Fatal("expected default toolset to be present")
1481+
}
1482+
1483+
expectedOrder := []string{"apple", "banana", "zoo"}
1484+
if diff := cmp.Diff(expectedOrder, defaultToolset.ToolNames); diff != "" {
1485+
t.Errorf("default toolset ToolNames mismatch (-want +got):\n%s", diff)
1486+
}
1487+
}

0 commit comments

Comments
 (0)