Class: McpToolkit::Gateway::Aggregator
- Inherits:
-
Object
- Object
- McpToolkit::Gateway::Aggregator
- Defined in:
- lib/mcp_toolkit/gateway/aggregator.rb
Overview
Aggregates the tool lists of all configured upstream MCP servers for a
gateway's tools/list, namespacing each tool as <app>__<tool>.
Per upstream the namespaced list is cached in config.cache_store
(config.upstream_list_ttl, default 15 min) and bustable via flush! /
flush!(key). On a cache miss the live pull runs per-upstream (one HTTP call
each); a failing/timeout upstream is omitted + logged, never breaking the list.
Why the cache is safe to share globally: an upstream's tools/list is
token-INDEPENDENT — it returns the same public tool definitions to every valid
caller and enforces scope only when a tool is CALLED (per-call authorization is
the upstream's job). So one caller's pull is a correct answer for all callers.
This is a registration CONTRACT, not an assumption: an upstream that filters its
list by the caller's privilege (e.g. hides superuser-only tools) MUST register
public_tool_list: false, which opts it out of this cache (pulled live per
request) so a privileged caller's list can never be served to an unprivileged
one. See McpToolkit::Gateway::UpstreamRegistry::Upstream.
Only a NON-EMPTY pull is ever cached. An empty or failed pull is almost always a
transient upstream hiccup (timeout, a session/handshake blip, a degenerate 200);
caching it would freeze a whole app's tools out of tools/list for the full TTL
for EVERY caller (a poisoned global cache), which is exactly the failure this
guards against. A stale empty already in the cache is treated as a miss and
re-pulled, so the aggregate self-heals as soon as the upstream returns tools.
Concurrency: upstreams are pulled CONCURRENTLY via concurrent-ruby futures. When
running inside Rails, each future is wrapped in Rails.application.executor so
it participates in the framework's per-request lifecycle (reloading, query
cache, connection checkout); a non-Rails host runs plain futures. Output order
follows the registry order.
Constant Summary collapse
- CACHE_KEY_PREFIX =
"mcp_toolkit:gateway:tools:"
Instance Method Summary collapse
- #flush!(key = nil) ⇒ Object
-
#initialize(config: McpToolkit.config) ⇒ Aggregator
constructor
A new instance of Aggregator.
-
#tool_definitions(bearer_token: nil) ⇒ Object
Namespaced tool definitions across all configured upstreams.
Constructor Details
#initialize(config: McpToolkit.config) ⇒ Aggregator
Returns a new instance of Aggregator.
38 39 40 |
# File 'lib/mcp_toolkit/gateway/aggregator.rb', line 38 def initialize(config: McpToolkit.config) @config = config end |
Instance Method Details
#flush!(key = nil) ⇒ Object
56 57 58 59 60 61 62 |
# File 'lib/mcp_toolkit/gateway/aggregator.rb', line 56 def flush!(key = nil) if key cache.delete(cache_key(key)) else config.upstreams.all.each { |upstream| cache.delete(cache_key(upstream.key)) } end end |
#tool_definitions(bearer_token: nil) ⇒ Object
Namespaced tool definitions across all configured upstreams. bearer_token
is used only on a cache miss, so the upstream can authenticate the list
request the same way it would a call. Upstreams are fetched concurrently;
output order follows the registry order.
46 47 48 49 50 51 52 53 54 |
# File 'lib/mcp_toolkit/gateway/aggregator.rb', line 46 def tool_definitions(bearer_token: nil) futures = config.upstreams.all.map do |upstream| Concurrent::Promises.future do within_executor { cached_or_live_definitions(upstream, bearer_token:) } end end futures.flat_map(&:value!) end |