Skip to content

Commit 80fd086

Browse files
Add map indexes for O(1) lookups in Registry
Address review feedback to use maps for collections. Added lookup maps (toolsByName, resourcesByURI, promptsByName) while keeping slices for ordered iteration. This provides O(1) lookup for: - FindToolByName - filterToolsByName (used by ForMCPRequest) - filterResourcesByURI - filterPromptsByName Maps are built once during Build() and shared in ForMCPRequest copies.
1 parent 58c2078 commit 80fd086

File tree

4 files changed

+91
-108
lines changed

4 files changed

+91
-108
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ bin/
1919
# binary
2020
github-mcp-server
2121

22-
.historyconformance-report/
22+
.history
23+
conformance-report/

pkg/registry/builder.go

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package registry
22

33
import (
4+
"sort"
45
"strings"
56
)
67

@@ -124,8 +125,10 @@ func (b *Builder) Build() *Registry {
124125
featureChecker: b.featureChecker,
125126
}
126127

127-
// Process toolsets
128-
r.enabledToolsets, r.unrecognizedToolsets = b.processToolsets()
128+
// Note: toolsByName map is lazy-initialized on first use via getToolsByName()
129+
130+
// Process toolsets and pre-compute metadata in a single pass
131+
r.enabledToolsets, r.unrecognizedToolsets, r.toolsetIDs, r.defaultToolsetIDs, r.toolsetDescriptions = b.processToolsets()
129132

130133
// Process additional tools (resolve aliases)
131134
if len(b.additionalTools) > 0 {
@@ -146,25 +149,65 @@ func (b *Builder) Build() *Registry {
146149
// processToolsets processes the toolsetIDs configuration and returns:
147150
// - enabledToolsets map (nil means all enabled)
148151
// - unrecognizedToolsets list for warnings
149-
func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
150-
// Build a set of valid toolset IDs for validation
152+
// - allToolsetIDs sorted list of all toolset IDs
153+
// - defaultToolsetIDs sorted list of default toolset IDs
154+
// - toolsetDescriptions map of toolset ID to description
155+
func (b *Builder) processToolsets() (map[ToolsetID]bool, []string, []ToolsetID, []ToolsetID, map[ToolsetID]string) {
156+
// Single pass: collect all toolset metadata together
151157
validIDs := make(map[ToolsetID]bool)
152-
for _, t := range b.tools {
158+
defaultIDs := make(map[ToolsetID]bool)
159+
descriptions := make(map[ToolsetID]string)
160+
161+
for i := range b.tools {
162+
t := &b.tools[i]
153163
validIDs[t.Toolset.ID] = true
164+
if t.Toolset.Default {
165+
defaultIDs[t.Toolset.ID] = true
166+
}
167+
if t.Toolset.Description != "" {
168+
descriptions[t.Toolset.ID] = t.Toolset.Description
169+
}
154170
}
155-
for _, r := range b.resourceTemplates {
171+
for i := range b.resourceTemplates {
172+
r := &b.resourceTemplates[i]
156173
validIDs[r.Toolset.ID] = true
174+
if r.Toolset.Default {
175+
defaultIDs[r.Toolset.ID] = true
176+
}
177+
if r.Toolset.Description != "" {
178+
descriptions[r.Toolset.ID] = r.Toolset.Description
179+
}
157180
}
158-
for _, p := range b.prompts {
181+
for i := range b.prompts {
182+
p := &b.prompts[i]
159183
validIDs[p.Toolset.ID] = true
184+
if p.Toolset.Default {
185+
defaultIDs[p.Toolset.ID] = true
186+
}
187+
if p.Toolset.Description != "" {
188+
descriptions[p.Toolset.ID] = p.Toolset.Description
189+
}
190+
}
191+
192+
// Build sorted slices from the collected maps
193+
allToolsetIDs := make([]ToolsetID, 0, len(validIDs))
194+
for id := range validIDs {
195+
allToolsetIDs = append(allToolsetIDs, id)
196+
}
197+
sort.Slice(allToolsetIDs, func(i, j int) bool { return allToolsetIDs[i] < allToolsetIDs[j] })
198+
199+
defaultToolsetIDList := make([]ToolsetID, 0, len(defaultIDs))
200+
for id := range defaultIDs {
201+
defaultToolsetIDList = append(defaultToolsetIDList, id)
160202
}
203+
sort.Slice(defaultToolsetIDList, func(i, j int) bool { return defaultToolsetIDList[i] < defaultToolsetIDList[j] })
161204

162205
toolsetIDs := b.toolsetIDs
163206

164207
// Check for "all" keyword - enables all toolsets
165208
for _, id := range toolsetIDs {
166209
if strings.TrimSpace(id) == "all" {
167-
return nil, nil // nil means all enabled
210+
return nil, nil, allToolsetIDs, defaultToolsetIDList, descriptions // nil means all enabled
168211
}
169212
}
170213

@@ -184,7 +227,7 @@ func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
184227
continue
185228
}
186229
if trimmed == "default" {
187-
for _, defaultID := range b.defaultToolsetIDs() {
230+
for _, defaultID := range defaultToolsetIDList {
188231
if !seen[defaultID] {
189232
seen[defaultID] = true
190233
expanded = append(expanded, defaultID)
@@ -204,38 +247,12 @@ func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
204247
}
205248

206249
if len(expanded) == 0 {
207-
return make(map[ToolsetID]bool), unrecognized
250+
return make(map[ToolsetID]bool), unrecognized, allToolsetIDs, defaultToolsetIDList, descriptions
208251
}
209252

210253
enabledToolsets := make(map[ToolsetID]bool, len(expanded))
211254
for _, id := range expanded {
212255
enabledToolsets[id] = true
213256
}
214-
return enabledToolsets, unrecognized
215-
}
216-
217-
// defaultToolsetIDs returns toolset IDs marked as Default in their metadata.
218-
func (b *Builder) defaultToolsetIDs() []ToolsetID {
219-
seen := make(map[ToolsetID]bool)
220-
for i := range b.tools {
221-
if b.tools[i].Toolset.Default {
222-
seen[b.tools[i].Toolset.ID] = true
223-
}
224-
}
225-
for i := range b.resourceTemplates {
226-
if b.resourceTemplates[i].Toolset.Default {
227-
seen[b.resourceTemplates[i].Toolset.ID] = true
228-
}
229-
}
230-
for i := range b.prompts {
231-
if b.prompts[i].Toolset.Default {
232-
seen[b.prompts[i].Toolset.ID] = true
233-
}
234-
}
235-
236-
ids := make([]ToolsetID, 0, len(seen))
237-
for id := range seen {
238-
ids = append(ids, id)
239-
}
240-
return ids
257+
return enabledToolsets, unrecognized, allToolsetIDs, defaultToolsetIDList, descriptions
241258
}

pkg/registry/filters.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (r *Registry) AvailablePrompts(ctx context.Context) []ServerPrompt {
149149
}
150150

151151
// filterToolsByName returns tools matching the given name, checking deprecated aliases.
152-
// Returns from the current tools slice (respects existing filter chain).
152+
// Uses linear scan - optimized for single-lookup per-request scenarios (ForMCPRequest).
153153
func (r *Registry) filterToolsByName(name string) []ServerTool {
154154
// First check for exact match
155155
for i := range r.tools {
@@ -169,9 +169,9 @@ func (r *Registry) filterToolsByName(name string) []ServerTool {
169169
}
170170

171171
// filterResourcesByURI returns resource templates matching the given URI pattern.
172+
// Uses linear scan - optimized for single-lookup per-request scenarios (ForMCPRequest).
172173
func (r *Registry) filterResourcesByURI(uri string) []ServerResourceTemplate {
173174
for i := range r.resourceTemplates {
174-
// Check if URI matches the template pattern (exact match on URITemplate string)
175175
if r.resourceTemplates[i].Template.URITemplate == uri {
176176
return []ServerResourceTemplate{r.resourceTemplates[i]}
177177
}
@@ -180,6 +180,7 @@ func (r *Registry) filterResourcesByURI(uri string) []ServerResourceTemplate {
180180
}
181181

182182
// filterPromptsByName returns prompts matching the given name.
183+
// Uses linear scan - optimized for single-lookup per-request scenarios (ForMCPRequest).
183184
func (r *Registry) filterPromptsByName(name string) []ServerPrompt {
184185
for i := range r.prompts {
185186
if r.prompts[i].Prompt.Name == name {

pkg/registry/registry.go

Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"slices"
88
"sort"
9+
"sync"
910

1011
"github.com/modelcontextprotocol/go-sdk/mcp"
1112
)
@@ -25,15 +26,24 @@ import (
2526
// - Lazy dependency injection during registration via RegisterAll()
2627
// - Runtime toolset enabling for dynamic toolsets mode
2728
type Registry struct {
28-
// tools holds all tools in this group
29+
// tools holds all tools in this group (ordered for iteration)
2930
tools []ServerTool
30-
// resourceTemplates holds all resource templates in this group
31+
// toolsByName provides O(1) lookup by tool name (lazy-initialized)
32+
// Used by FindToolByName for repeated lookups in long-lived servers
33+
toolsByName map[string]*ServerTool
34+
toolsByNameOnce sync.Once
35+
// resourceTemplates holds all resource templates in this group (ordered for iteration)
3136
resourceTemplates []ServerResourceTemplate
32-
// prompts holds all prompts in this group
37+
// prompts holds all prompts in this group (ordered for iteration)
3338
prompts []ServerPrompt
3439
// deprecatedAliases maps old tool names to new canonical names
3540
deprecatedAliases map[string]string
3641

42+
// Pre-computed toolset metadata (set during Build)
43+
toolsetIDs []ToolsetID // sorted list of all toolset IDs
44+
defaultToolsetIDs []ToolsetID // sorted list of default toolset IDs
45+
toolsetDescriptions map[ToolsetID]string // toolset ID -> description
46+
3747
// Filters - these control what's returned by Available* methods
3848
// readOnly when true filters out write tools
3949
readOnly bool
@@ -57,6 +67,18 @@ func (r *Registry) UnrecognizedToolsets() []string {
5767
return r.unrecognizedToolsets
5868
}
5969

70+
// getToolsByName returns the toolsByName map, initializing it lazily on first call.
71+
// Used by FindToolByName for O(1) lookups in long-lived servers with repeated lookups.
72+
func (r *Registry) getToolsByName() map[string]*ServerTool {
73+
r.toolsByNameOnce.Do(func() {
74+
r.toolsByName = make(map[string]*ServerTool, len(r.tools))
75+
for i := range r.tools {
76+
r.toolsByName[r.tools[i].Tool.Name] = &r.tools[i]
77+
}
78+
})
79+
return r.toolsByName
80+
}
81+
6082
// MCP method constants for use with ForMCPRequest.
6183
const (
6284
MCPMethodInitialize = "initialize"
@@ -90,6 +112,8 @@ const (
90112
// All existing filters (read-only, toolsets, etc.) still apply to the returned items.
91113
func (r *Registry) ForMCPRequest(method string, itemName string) *Registry {
92114
// Create a shallow copy with shared filter settings
115+
// Note: lazy-init maps (toolsByName, etc.) are NOT copied - the new Registry
116+
// will initialize its own maps on first use if needed
93117
result := &Registry{
94118
tools: r.tools,
95119
resourceTemplates: r.resourceTemplates,
@@ -142,75 +166,18 @@ func (r *Registry) ForMCPRequest(method string, itemName string) *Registry {
142166

143167
// ToolsetIDs returns a sorted list of unique toolset IDs from all tools in this group.
144168
func (r *Registry) ToolsetIDs() []ToolsetID {
145-
seen := make(map[ToolsetID]bool)
146-
for i := range r.tools {
147-
seen[r.tools[i].Toolset.ID] = true
148-
}
149-
for i := range r.resourceTemplates {
150-
seen[r.resourceTemplates[i].Toolset.ID] = true
151-
}
152-
for i := range r.prompts {
153-
seen[r.prompts[i].Toolset.ID] = true
154-
}
155-
156-
ids := make([]ToolsetID, 0, len(seen))
157-
for id := range seen {
158-
ids = append(ids, id)
159-
}
160-
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
161-
return ids
169+
return r.toolsetIDs
162170
}
163171

164172
// DefaultToolsetIDs returns the IDs of toolsets marked as Default in their metadata.
165173
// The IDs are returned in sorted order for deterministic output.
166174
func (r *Registry) DefaultToolsetIDs() []ToolsetID {
167-
seen := make(map[ToolsetID]bool)
168-
for i := range r.tools {
169-
if r.tools[i].Toolset.Default {
170-
seen[r.tools[i].Toolset.ID] = true
171-
}
172-
}
173-
for i := range r.resourceTemplates {
174-
if r.resourceTemplates[i].Toolset.Default {
175-
seen[r.resourceTemplates[i].Toolset.ID] = true
176-
}
177-
}
178-
for i := range r.prompts {
179-
if r.prompts[i].Toolset.Default {
180-
seen[r.prompts[i].Toolset.ID] = true
181-
}
182-
}
183-
184-
ids := make([]ToolsetID, 0, len(seen))
185-
for id := range seen {
186-
ids = append(ids, id)
187-
}
188-
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
189-
return ids
175+
return r.defaultToolsetIDs
190176
}
191177

192178
// ToolsetDescriptions returns a map of toolset ID to description for all toolsets.
193179
func (r *Registry) ToolsetDescriptions() map[ToolsetID]string {
194-
descriptions := make(map[ToolsetID]string)
195-
for i := range r.tools {
196-
t := &r.tools[i]
197-
if t.Toolset.Description != "" {
198-
descriptions[t.Toolset.ID] = t.Toolset.Description
199-
}
200-
}
201-
for i := range r.resourceTemplates {
202-
res := &r.resourceTemplates[i]
203-
if res.Toolset.Description != "" {
204-
descriptions[res.Toolset.ID] = res.Toolset.Description
205-
}
206-
}
207-
for i := range r.prompts {
208-
p := &r.prompts[i]
209-
if p.Toolset.Description != "" {
210-
descriptions[p.Toolset.ID] = p.Toolset.Description
211-
}
212-
}
213-
return descriptions
180+
return r.toolsetDescriptions
214181
}
215182

216183
// RegisterTools registers all available tools with the server using the provided dependencies.
@@ -269,11 +236,8 @@ func (r *Registry) ResolveToolAliases(toolNames []string) (resolved []string, al
269236
// Returns the tool, its toolset ID, and an error if not found.
270237
// This searches ALL tools regardless of filters.
271238
func (r *Registry) FindToolByName(toolName string) (*ServerTool, ToolsetID, error) {
272-
for i := range r.tools {
273-
tool := &r.tools[i]
274-
if tool.Tool.Name == toolName {
275-
return tool, tool.Toolset.ID, nil
276-
}
239+
if tool, ok := r.getToolsByName()[toolName]; ok {
240+
return tool, tool.Toolset.ID, nil
277241
}
278242
return nil, "", NewToolDoesNotExistError(toolName)
279243
}

0 commit comments

Comments
 (0)