Skip to content

Commit 41189d1

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 41189d1

File tree

4 files changed

+87
-61
lines changed

4 files changed

+87
-61
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: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ func (b *Builder) Build() *Registry {
124124
featureChecker: b.featureChecker,
125125
}
126126

127+
// Note: lookup maps (toolsByName, resourcesByURI, promptsByName) are
128+
// lazy-initialized on first use via getToolsByName(), etc.
129+
127130
// Process toolsets
128131
r.enabledToolsets, r.unrecognizedToolsets = b.processToolsets()
129132

@@ -147,16 +150,30 @@ func (b *Builder) Build() *Registry {
147150
// - enabledToolsets map (nil means all enabled)
148151
// - unrecognizedToolsets list for warnings
149152
func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
150-
// Build a set of valid toolset IDs for validation
153+
// Single pass: collect valid IDs and default IDs together
151154
validIDs := make(map[ToolsetID]bool)
152-
for _, t := range b.tools {
153-
validIDs[t.Toolset.ID] = true
155+
defaultIDs := make(map[ToolsetID]bool)
156+
157+
for i := range b.tools {
158+
id := b.tools[i].Toolset.ID
159+
validIDs[id] = true
160+
if b.tools[i].Toolset.Default {
161+
defaultIDs[id] = true
162+
}
154163
}
155-
for _, r := range b.resourceTemplates {
156-
validIDs[r.Toolset.ID] = true
164+
for i := range b.resourceTemplates {
165+
id := b.resourceTemplates[i].Toolset.ID
166+
validIDs[id] = true
167+
if b.resourceTemplates[i].Toolset.Default {
168+
defaultIDs[id] = true
169+
}
157170
}
158-
for _, p := range b.prompts {
159-
validIDs[p.Toolset.ID] = true
171+
for i := range b.prompts {
172+
id := b.prompts[i].Toolset.ID
173+
validIDs[id] = true
174+
if b.prompts[i].Toolset.Default {
175+
defaultIDs[id] = true
176+
}
160177
}
161178

162179
toolsetIDs := b.toolsetIDs
@@ -184,7 +201,7 @@ func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
184201
continue
185202
}
186203
if trimmed == "default" {
187-
for _, defaultID := range b.defaultToolsetIDs() {
204+
for defaultID := range defaultIDs {
188205
if !seen[defaultID] {
189206
seen[defaultID] = true
190207
expanded = append(expanded, defaultID)
@@ -213,29 +230,3 @@ func (b *Builder) processToolsets() (map[ToolsetID]bool, []string) {
213230
}
214231
return enabledToolsets, unrecognized
215232
}
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
241-
}

pkg/registry/filters.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,40 +151,32 @@ func (r *Registry) AvailablePrompts(ctx context.Context) []ServerPrompt {
151151
// filterToolsByName returns tools matching the given name, checking deprecated aliases.
152152
// Returns from the current tools slice (respects existing filter chain).
153153
func (r *Registry) filterToolsByName(name string) []ServerTool {
154-
// First check for exact match
155-
for i := range r.tools {
156-
if r.tools[i].Tool.Name == name {
157-
return []ServerTool{r.tools[i]}
158-
}
154+
toolsByName := r.getToolsByName()
155+
// First check for exact match using map
156+
if tool, ok := toolsByName[name]; ok {
157+
return []ServerTool{*tool}
159158
}
160159
// Check if name is a deprecated alias
161160
if canonical, isAlias := r.deprecatedAliases[name]; isAlias {
162-
for i := range r.tools {
163-
if r.tools[i].Tool.Name == canonical {
164-
return []ServerTool{r.tools[i]}
165-
}
161+
if tool, ok := toolsByName[canonical]; ok {
162+
return []ServerTool{*tool}
166163
}
167164
}
168165
return []ServerTool{}
169166
}
170167

171168
// filterResourcesByURI returns resource templates matching the given URI pattern.
172169
func (r *Registry) filterResourcesByURI(uri string) []ServerResourceTemplate {
173-
for i := range r.resourceTemplates {
174-
// Check if URI matches the template pattern (exact match on URITemplate string)
175-
if r.resourceTemplates[i].Template.URITemplate == uri {
176-
return []ServerResourceTemplate{r.resourceTemplates[i]}
177-
}
170+
if res, ok := r.getResourcesByURI()[uri]; ok {
171+
return []ServerResourceTemplate{*res}
178172
}
179173
return []ServerResourceTemplate{}
180174
}
181175

182176
// filterPromptsByName returns prompts matching the given name.
183177
func (r *Registry) filterPromptsByName(name string) []ServerPrompt {
184-
for i := range r.prompts {
185-
if r.prompts[i].Prompt.Name == name {
186-
return []ServerPrompt{r.prompts[i]}
187-
}
178+
if prompt, ok := r.getPromptsByName()[name]; ok {
179+
return []ServerPrompt{*prompt}
188180
}
189181
return []ServerPrompt{}
190182
}

pkg/registry/registry.go

Lines changed: 50 additions & 8 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,12 +26,21 @@ 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+
toolsByName map[string]*ServerTool
33+
toolsByNameOnce sync.Once
34+
// resourceTemplates holds all resource templates in this group (ordered for iteration)
3135
resourceTemplates []ServerResourceTemplate
32-
// prompts holds all prompts in this group
36+
// resourcesByURI provides O(1) lookup by URI template (lazy-initialized)
37+
resourcesByURI map[string]*ServerResourceTemplate
38+
resourcesByURIOnce sync.Once
39+
// prompts holds all prompts in this group (ordered for iteration)
3340
prompts []ServerPrompt
41+
// promptsByName provides O(1) lookup by prompt name (lazy-initialized)
42+
promptsByName map[string]*ServerPrompt
43+
promptsByNameOnce sync.Once
3444
// deprecatedAliases maps old tool names to new canonical names
3545
deprecatedAliases map[string]string
3646

@@ -57,6 +67,39 @@ func (r *Registry) UnrecognizedToolsets() []string {
5767
return r.unrecognizedToolsets
5868
}
5969

70+
// getToolsByName returns the toolsByName map, initializing it lazily on first call.
71+
func (r *Registry) getToolsByName() map[string]*ServerTool {
72+
r.toolsByNameOnce.Do(func() {
73+
r.toolsByName = make(map[string]*ServerTool, len(r.tools))
74+
for i := range r.tools {
75+
r.toolsByName[r.tools[i].Tool.Name] = &r.tools[i]
76+
}
77+
})
78+
return r.toolsByName
79+
}
80+
81+
// getResourcesByURI returns the resourcesByURI map, initializing it lazily on first call.
82+
func (r *Registry) getResourcesByURI() map[string]*ServerResourceTemplate {
83+
r.resourcesByURIOnce.Do(func() {
84+
r.resourcesByURI = make(map[string]*ServerResourceTemplate, len(r.resourceTemplates))
85+
for i := range r.resourceTemplates {
86+
r.resourcesByURI[r.resourceTemplates[i].Template.URITemplate] = &r.resourceTemplates[i]
87+
}
88+
})
89+
return r.resourcesByURI
90+
}
91+
92+
// getPromptsByName returns the promptsByName map, initializing it lazily on first call.
93+
func (r *Registry) getPromptsByName() map[string]*ServerPrompt {
94+
r.promptsByNameOnce.Do(func() {
95+
r.promptsByName = make(map[string]*ServerPrompt, len(r.prompts))
96+
for i := range r.prompts {
97+
r.promptsByName[r.prompts[i].Prompt.Name] = &r.prompts[i]
98+
}
99+
})
100+
return r.promptsByName
101+
}
102+
60103
// MCP method constants for use with ForMCPRequest.
61104
const (
62105
MCPMethodInitialize = "initialize"
@@ -90,6 +133,8 @@ const (
90133
// All existing filters (read-only, toolsets, etc.) still apply to the returned items.
91134
func (r *Registry) ForMCPRequest(method string, itemName string) *Registry {
92135
// Create a shallow copy with shared filter settings
136+
// Note: lazy-init maps (toolsByName, etc.) are NOT copied - the new Registry
137+
// will initialize its own maps on first use if needed
93138
result := &Registry{
94139
tools: r.tools,
95140
resourceTemplates: r.resourceTemplates,
@@ -269,11 +314,8 @@ func (r *Registry) ResolveToolAliases(toolNames []string) (resolved []string, al
269314
// Returns the tool, its toolset ID, and an error if not found.
270315
// This searches ALL tools regardless of filters.
271316
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-
}
317+
if tool, ok := r.getToolsByName()[toolName]; ok {
318+
return tool, tool.Toolset.ID, nil
277319
}
278320
return nil, "", NewToolDoesNotExistError(toolName)
279321
}

0 commit comments

Comments
 (0)