-
Notifications
You must be signed in to change notification settings - Fork 3.2k
perf: add inventory cache for stateless server patterns #1636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add CachedInventory to build tool/resource/prompt definitions once at startup rather than per-request. This is particularly useful for the remote server pattern where a new server instance is created per request. Key features: - InitInventoryCache(t) initializes the cache once at startup - InitInventoryCacheWithExtras(t, tools, resources, prompts) allows injecting additional items (e.g., remote-only Copilot tools) - CachedInventoryBuilder() returns a builder with pre-cached definitions - Per-request configuration (read-only, toolsets, feature flags) still works - Thread-safe via sync.Once - Backward compatible: NewInventory(t) still works without caching This addresses the performance concern raised in go-sdk PR #685 at a higher level by caching the entire []ServerTool slice rather than individual schemas. Related: modelcontextprotocol/go-sdk#685
718046c to
a456b82
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds an inventory caching mechanism to optimize performance in stateless server deployments where a new server instance is created per request. The cache eliminates redundant rebuilding of ~130 tool definitions and their JSON schemas on every request.
Key Changes
- Introduces
CachedInventorywithsync.Oncefor thread-safe one-time initialization of tool/resource/prompt definitions - Provides
InitInventoryCache()for explicit startup initialization andCachedInventoryBuilder()for cached builder access - Maintains full backward compatibility with existing
NewInventory()API
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| pkg/github/inventory_cache.go | Implements the caching mechanism with global singleton, sync.Once initialization, and builder factory methods |
| pkg/github/inventory_cache_test.go | Comprehensive test coverage including initialization, idempotency, auto-initialization, per-request filtering, and comparison with uncached behavior |
| }) | ||
| } | ||
|
|
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ResetInventoryCache function creates a race condition. After resetting globalInventoryCache to a new instance, the old sync.Once in the previous instance is lost. If another goroutine was in the middle of calling init() on the old instance, it would complete initialization on the old instance while new callers would get the new uninitialized instance. Additionally, resetting doesn't prevent the old sync.Once from keeping the "already called" state.
While the comment correctly warns this is test-only and not thread-safe, a safer approach would be to reset the fields inside the existing CachedInventory struct rather than replacing the entire global variable. However, sync.Once cannot be reset by design.
Consider adding a mutex-based solution for tests or document more clearly that tests must ensure serial execution when calling ResetInventoryCache.
| "github.com/github/github-mcp-server/pkg/translations" | ||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests in this file intentionally do not use t.Parallel() because they mutate global state via ResetInventoryCache(). This is correct, but it should be documented with a comment at the top of the test file to explain why these tests must run serially, especially since most other test files in this package use t.Parallel().
| "github.com/stretchr/testify/require" | |
| // NOTE: Tests in this file intentionally do NOT use t.Parallel(). | |
| // They mutate process-wide inventory cache state via ResetInventoryCache / | |
| // InitInventoryCache / CachedInventoryBuilder, so running them in parallel | |
| // could cause test flakiness and data races. Keep these tests serial even | |
| // though most other tests in this package use t.Parallel(). |
| if len(extraTools) > 0 { | ||
| c.tools = append(c.tools, extraTools...) | ||
| } | ||
| if len(extraResources) > 0 { |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IsCacheInitialized check is unreliable. It checks if len(globalInventoryCache.tools) > 0, but this has two problems:
- If AllTools(t) returns an empty slice (which shouldn't happen in practice but is theoretically possible), IsCacheInitialized would return false even after successful initialization
- After ResetInventoryCache() is called, the check looks at the new instance which will have empty tools, making it work correctly. However, the comment at line 106 says "We can't directly check sync.Once state" which suggests the intent is to check if the Once has fired, not just if tools are populated.
A more reliable approach would be to add an explicit boolean flag that's set to true inside the once.Do callback, which would accurately reflect whether initialization has occurred regardless of whether AllTools returns an empty slice.
| // github.InitInventoryCacheWithExtras( | ||
| // translations.NullTranslationHelper, |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation is contradictory. Line 76 states "The cache must be initialized via InitInventoryCache before calling this function" but line 77 immediately says "If the cache is not initialized, this will initialize it with NullTranslationHelper."
This creates confusion about whether initialization is required or optional. Consider rephrasing to clarify the behavior, such as: "The cache should typically be initialized via InitInventoryCache before calling this function. If not already initialized, it will automatically initialize with NullTranslationHelper as a fallback."
pkg/github/inventory_cache.go
Outdated
| ) | ||
|
|
||
| // CachedInventory provides a cached inventory builder that builds tool definitions | ||
| // only once, regardless of how many times NewInventoryBuilder is called. |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment uses inconsistent terminology. The comment says "NewInventoryBuilder" but the actual function is called "CachedInventoryBuilder". This creates confusion about which function is being referred to.
| // only once, regardless of how many times NewInventoryBuilder is called. | |
| // only once, regardless of how many times CachedInventoryBuilder is called. |
- 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()
Summary
Add
CachedInventoryto build tool/resource/prompt definitions once at startup rather than per-request. This is particularly useful for the remote server pattern where a new server instance is created per request.Problem
In stateless deployments (like the remote MCP server), a new server and inventory is created for each incoming request. This means
AllTools(t),AllResources(t), andAllPrompts(t)are called repeatedly, rebuilding all ~130 tool definitions including JSON schema generation on every request.Related: modelcontextprotocol/go-sdk#685
Solution
Add a
CachedInventorythat:sync.Oncefor thread-safe initializationAPI
Key Features
InitInventoryCache(t)- Called once at startup with your translatorCachedInventoryBuilder()- Returns a builder pre-populated with cached definitionssync.OnceNewInventory(t)still works without cachingWhy This Works
The elegance is preserved because:
GetMe(t),GetTeams(t)pattern stays the sameAvailableTools(ctx), not at definition timeNullTranslationHelperanyway, this is fineFor Remote Server Integration
Testing
inventory_cache_test.goscript/lintpasses