Skip to content

Commit c7dc8fd

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 c7dc8fd

File tree

4 files changed

+54
-45
lines changed

4 files changed

+54
-45
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: 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: 24 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,11 +26,15 @@ 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
@@ -57,6 +62,18 @@ func (r *Registry) UnrecognizedToolsets() []string {
5762
return r.unrecognizedToolsets
5863
}
5964

65+
// getToolsByName returns the toolsByName map, initializing it lazily on first call.
66+
// Used by FindToolByName for O(1) lookups in long-lived servers with repeated lookups.
67+
func (r *Registry) getToolsByName() map[string]*ServerTool {
68+
r.toolsByNameOnce.Do(func() {
69+
r.toolsByName = make(map[string]*ServerTool, len(r.tools))
70+
for i := range r.tools {
71+
r.toolsByName[r.tools[i].Tool.Name] = &r.tools[i]
72+
}
73+
})
74+
return r.toolsByName
75+
}
76+
6077
// MCP method constants for use with ForMCPRequest.
6178
const (
6279
MCPMethodInitialize = "initialize"
@@ -90,6 +107,8 @@ const (
90107
// All existing filters (read-only, toolsets, etc.) still apply to the returned items.
91108
func (r *Registry) ForMCPRequest(method string, itemName string) *Registry {
92109
// Create a shallow copy with shared filter settings
110+
// Note: lazy-init maps (toolsByName, etc.) are NOT copied - the new Registry
111+
// will initialize its own maps on first use if needed
93112
result := &Registry{
94113
tools: r.tools,
95114
resourceTemplates: r.resourceTemplates,
@@ -269,11 +288,8 @@ func (r *Registry) ResolveToolAliases(toolNames []string) (resolved []string, al
269288
// Returns the tool, its toolset ID, and an error if not found.
270289
// This searches ALL tools regardless of filters.
271290
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-
}
291+
if tool, ok := r.getToolsByName()[toolName]; ok {
292+
return tool, tool.Toolset.ID, nil
277293
}
278294
return nil, "", NewToolDoesNotExistError(toolName)
279295
}

0 commit comments

Comments
 (0)