Class: Pikuri::Mcp::Synthesizer
- Inherits:
-
Object
- Object
- Pikuri::Mcp::Synthesizer
- Defined in:
- lib/pikuri/mcp/synthesizer.rb
Overview
One-shot LLM synthesis of a short "what does this MCP server do"
description for the <available_mcps> block, paired with the on-disk
Cache. Servers just calls #call and treats nil as "fall back".
Follows the boot-pass cancel/degrade policy: Cancellable::Cancelled from
the Thinker pre-call check propagates (not caught by the StandardError
rescue) to abort startup; any other StandardError (flaky LLM, cache
write failure) is WARN-logged and returns nil, so a transient blip
doesn't drop a server from the listing. See pikuri-mcp/DESIGN.md.
Constant Summary collapse
- PROMPT_VERSION =
Bump when #build_prompt changes meaningfully. Cache folds it into the key fingerprint so a bump invalidates the previous prompt's entries without anyone +rm+-ing the cache.
2
Instance Method Summary collapse
-
#call(entry:, client:, tools:) ⇒ String?
Produce the description for one server.
- #initialize(transport: nil, cancellable: nil, thinker: nil, cache: nil) ⇒ Synthesizer constructor
Constructor Details
#initialize(transport: nil, cancellable: nil, thinker: nil, cache: nil) ⇒ Synthesizer
36 37 38 39 40 41 42 43 |
# File 'lib/pikuri/mcp/synthesizer.rb', line 36 def initialize(transport: nil, cancellable: nil, thinker: nil, cache: nil) raise ArgumentError, 'pass exactly one of transport: or thinker:' if transport.nil? == thinker.nil? raise ArgumentError, 'cancellable: only applies to the transport: path' if thinker && cancellable @thinker = thinker || Thinker.new(transport: transport, cancellable: cancellable) @cache = cache || (transport ? Cache.new(model_id: transport.model, prompt_version: PROMPT_VERSION) : Cache::NULL) end |
Instance Method Details
#call(entry:, client:, tools:) ⇒ String?
Produce the description for one server. Returns the cleaned
description String, or nil when the thinker raised
StandardError, returned blank, or otherwise failed to
produce anything usable. Pikuri::Mcp::Servers#resolve_description
treats nil as "fall back to serverInfo.name."
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/pikuri/mcp/synthesizer.rb', line 58 def call(entry:, client:, tools:) raw = @cache.fetch(entry: entry, client: client, tools: tools) do # Only fires on cache miss — synthesis is the slow path (LLM # round-trip, easily 30+ s on a local model); a heads-up is # warranted so the user doesn't wonder if pikuri is hung. # Cache hits skip this and the thinker.call entirely. LOGGER.info("Synthesizing description for MCP server #{entry.id.inspect}, please wait...") @thinker.call(build_prompt(entry, tools)) end cleaned = raw.to_s.strip.gsub(/\s+/, ' ') return nil if cleaned.empty? cleaned rescue Agent::Control::Cancellable::Cancelled raise rescue StandardError => e LOGGER.warn( "MCP description synthesis failed for #{entry.id.inspect} " \ "(#{e.class}: #{e.}); falling back to serverInfo.name." ) nil end |