Files
github--github-mcp-server/pkg/github/inventory_cache.go
Sam Morrow 1d05b3285e
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
License Check / license-check (push) Has been cancelled
address review feedback
- Fix inconsistent terminology (NewInventoryBuilder -> CachedInventoryBuilder)
- Add explicit initialized flag for reliable state checking
- Clarify contradictory documentation about initialization requirement
- Improve ResetInventoryCache warning about thread safety
- Add comment explaining why tests don't use t.Parallel()
2025-12-17 23:06:41 +01:00

166 lines
5.8 KiB
Go

package github
import (
"sync"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
)
// CachedInventory provides a cached inventory builder that builds tool definitions
// only once, regardless of how many times CachedInventoryBuilder is called.
//
// This is particularly useful for stateless server patterns (like the remote server)
// where a new server instance is created per request. Without caching, every request
// would rebuild all ~130 tool definitions including JSON schema generation, causing
// significant performance overhead.
//
// Usage:
//
// // Option 1: Initialize once at startup with your translator
// github.InitInventoryCache(myTranslator)
//
// // Then get pre-built inventory on each request
// inv := github.CachedInventoryBuilder().
// WithReadOnly(cfg.ReadOnly).
// WithToolsets(cfg.Toolsets).
// Build()
//
// // Option 2: Use NewInventory which doesn't use the cache (legacy behavior)
// inv := github.NewInventory(myTranslator).Build()
//
// The cache stores the built []ServerTool, []ServerResourceTemplate, and []ServerPrompt.
// Per-request configuration (read-only, toolsets, feature flags, filters) is still
// applied when building the Inventory from the cached data.
type CachedInventory struct {
once sync.Once
initialized bool // set to true after init completes
tools []inventory.ServerTool
resources []inventory.ServerResourceTemplate
prompts []inventory.ServerPrompt
}
// global singleton for caching
var globalInventoryCache = &CachedInventory{}
// InitInventoryCache initializes the global inventory cache with the given translator.
// This should be called once at startup before any requests are processed.
// It's safe to call multiple times - only the first call has any effect.
//
// For the local server, this is typically called with the configured translator.
// For the remote server, use translations.NullTranslationHelper since translations
// aren't needed per-request.
//
// Example:
//
// func main() {
// t, _ := translations.TranslationHelper()
// github.InitInventoryCache(t)
// // ... start server
// }
func InitInventoryCache(t translations.TranslationHelperFunc) {
globalInventoryCache.init(t, nil, nil, nil)
}
// InitInventoryCacheWithExtras initializes the global inventory cache with the given
// translator plus additional tools, resources, and prompts.
//
// This is useful for the remote server which has additional tools (e.g., Copilot tools)
// that aren't part of the base github-mcp-server package.
//
// The extra items are appended to the base items from AllTools/AllResources/AllPrompts.
// It's safe to call multiple times - only the first call has any effect.
//
// Example:
//
// func init() {
// github.InitInventoryCacheWithExtras(
// translations.NullTranslationHelper,
// remoteOnlyTools, // []inventory.ServerTool
// remoteOnlyResources, // []inventory.ServerResourceTemplate
// remoteOnlyPrompts, // []inventory.ServerPrompt
// )
// }
func InitInventoryCacheWithExtras(
t translations.TranslationHelperFunc,
extraTools []inventory.ServerTool,
extraResources []inventory.ServerResourceTemplate,
extraPrompts []inventory.ServerPrompt,
) {
globalInventoryCache.init(t, extraTools, extraResources, extraPrompts)
}
// init initializes the cache with the given translator and optional extras (sync.Once protected).
func (c *CachedInventory) init(
t translations.TranslationHelperFunc,
extraTools []inventory.ServerTool,
extraResources []inventory.ServerResourceTemplate,
extraPrompts []inventory.ServerPrompt,
) {
c.once.Do(func() {
c.tools = AllTools(t)
c.resources = AllResources(t)
c.prompts = AllPrompts(t)
// Append extra items if provided
if len(extraTools) > 0 {
c.tools = append(c.tools, extraTools...)
}
if len(extraResources) > 0 {
c.resources = append(c.resources, extraResources...)
}
if len(extraPrompts) > 0 {
c.prompts = append(c.prompts, extraPrompts...)
}
c.initialized = true
})
}
// CachedInventoryBuilder returns an inventory.Builder pre-populated with cached
// tool/resource/prompt definitions.
//
// The cache should typically be initialized via InitInventoryCache at startup.
// If not already initialized when this function is called, it will automatically
// initialize with NullTranslationHelper as a fallback.
//
// Per-request configuration can still be applied via the builder methods:
// - WithReadOnly(bool) - filter to read-only tools
// - WithToolsets([]string) - enable specific toolsets
// - WithTools([]string) - enable specific tools
// - WithFeatureChecker(func) - per-request feature flag evaluation
// - WithFilter(func) - custom filtering
//
// Example:
//
// inv := github.CachedInventoryBuilder().
// WithReadOnly(cfg.ReadOnly).
// WithToolsets(cfg.EnabledToolsets).
// WithFeatureChecker(createFeatureChecker(cfg.EnabledFeatures)).
// Build()
func CachedInventoryBuilder() *inventory.Builder {
// Ensure cache is initialized (with NullTranslationHelper as fallback)
globalInventoryCache.init(translations.NullTranslationHelper, nil, nil, nil)
return inventory.NewBuilder().
SetTools(globalInventoryCache.tools).
SetResources(globalInventoryCache.resources).
SetPrompts(globalInventoryCache.prompts)
}
// IsCacheInitialized returns true if the inventory cache has been initialized.
// This is primarily useful for testing.
func IsCacheInitialized() bool {
return globalInventoryCache.initialized
}
// ResetInventoryCache resets the global inventory cache, allowing it to be
// reinitialized with a different translator.
//
// WARNING: This function is for testing only. It is NOT thread-safe and must only
// be called when no other goroutines are accessing the cache. Tests using this
// function must not use t.Parallel().
func ResetInventoryCache() {
globalInventoryCache = &CachedInventory{}
}