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
2728type 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.
6183const (
6284 MCPMethodInitialize = "initialize"
@@ -90,6 +112,8 @@ const (
90112// All existing filters (read-only, toolsets, etc.) still apply to the returned items.
91113func (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.
144168func (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.
166174func (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.
193179func (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.
271238func (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