Class: Pikuri::Mcp::Cache

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/mcp/cache.rb

Overview

On-disk cache for the per-server descriptions Servers#try_synthesize_description pays an LLM round-trip to produce. Wraps UrlCache for storage; this class owns the *key calculation* — the fingerprint of every input that could change the synthesized output, so an unchanged surface short-circuits to the stored description and a changed surface auto-invalidates.

What goes into the key

The key hashes a canonical JSON serialization of:

  • model_id — synthesis quality varies by model; cached output from a different model should not be served.

  • prompt_version — Servers::PROMPT_VERSION. Bumped when the synthesizer prompt changes meaningfully so old cache entries stop being served without having to delete the cache file.

  • Transport descriptor — for Registry::StdioEntry, the argv; for Registry::HttpEntry, the URL.

  • Server name + version from client.server_info.

  • Full tools surface: every tool’s name, description, and input_schema — including nested property names, types, and per-property descriptions. The surface is canonicalised (recursive sort) so a server that reorders its tools array or its schema keys doesn’t blow the cache.

The server’s id from the registry is intentionally not part of the key — renaming a server entry in the registry doesn’t change what the server actually does, and the id appears only as a passing reference in the synth prompt, never in the produced description.

Storage

Reuses UrlCache with a ttl: Float::INFINITY. There is no time-based expiry — cache entries are valid forever, until the keyed surface changes. To force a rebuild of everything, rm the DIR directory (or bump Servers::PROMPT_VERSION).

Fail-soft contract

#fetch mirrors UrlCache#fetch — yields on miss, returns the block’s result, and persists it. Servers additionally rescues StandardError around the #fetch call to keep startup robust against a corrupt cache file or a writer-side error.

Constant Summary collapse

DIR =

On-disk root. Sibling of UrlCache::ROOT_DIR so all of pikuri’s caches live under one path.

File.join(File.dirname(UrlCache::ROOT_DIR), 'mcp_descriptions').freeze

Instance Method Summary collapse

Constructor Details

#initialize(model_id:, prompt_version:, dir: DIR) ⇒ Cache

Returns a new instance of Cache.

Parameters:

  • model_id (String, nil)

    the synthesizer model id; folded into the cache key so a model swap doesn’t serve stale output

  • prompt_version (Integer)

    Servers::PROMPT_VERSION; folded into the key so a prompt edit invalidates the world

  • dir (String) (defaults to: DIR)

    storage directory; defaults to DIR



66
67
68
69
70
# File 'lib/pikuri/mcp/cache.rb', line 66

def initialize(model_id:, prompt_version:, dir: DIR)
  @model_id = model_id
  @prompt_version = prompt_version
  @url_cache = UrlCache.new(ttl: Float::INFINITY, dir: dir)
end

Instance Method Details

#fetch(entry:, client:, tools:, &block) ⇒ String

Return the cached description for the given (entry, client, tools) triple if a fresh entry exists, otherwise yield to compute it (typically a Agent.think call inside Servers#try_synthesize_description), persist the result, and return it.

Parameters:

Yield Returns:

  • (String)

    freshly-computed description

Returns:

  • (String)

    cached or freshly-computed description



83
84
85
# File 'lib/pikuri/mcp/cache.rb', line 83

def fetch(entry:, client:, tools:, &block)
  @url_cache.fetch(key_for(entry, client, tools), &block)
end

#key_for(entry, client, tools) ⇒ String

Compute the canonical fingerprint string for a (entry, client, tools) triple. The fingerprint is what UrlCache SHA-256s into the on-disk filename; we don’t hash here so the raw JSON is inspectable in tests.

Returns:

  • (String)

    canonical JSON over the keyed inputs



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/pikuri/mcp/cache.rb', line 93

def key_for(entry, client, tools)
  info = (client.server_info || {})['serverInfo'] || {}
  fingerprint = {
    'model_id' => @model_id,
    'prompt_version' => @prompt_version,
    'transport' => transport_descriptor(entry),
    'server_name' => info['name'],
    'server_version' => info['version'],
    'tools' => tools_descriptor(tools)
  }
  JSON.generate(canonicalize(fingerprint))
end