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 that persists the result. Owns the prompt, its version, the cleanup (whitespace collapse), and the fail-soft contract — Servers just calls #call and treats a nil return as “fall back.”
Cancellation
Cancellable::Cancelled raised from inside the thinker (the Thinker pre-call check) propagates up through #call (it’s specifically not caught by the StandardError rescue) so a user’s Ctrl+C during a boot-time synthesis pass aborts startup cleanly rather than being silently logged as a “synthesis failure.”
Fail-soft on errors
Any other StandardError — a flaky LLM, garbage JSON, a cache write failure — is logged at WARN and #call returns nil. Pikuri::Mcp::Servers#resolve_description treats nil as the signal to fall back to serverInfo.name, so a single transient blip doesn’t take a server out of the <available_mcps> listing.
Constant Summary collapse
- PROMPT_VERSION =
Bump when #build_prompt changes meaningfully. Cache folds this into its key fingerprint via the
prompt_version:initializer kwarg, so a bump invalidates every cached entry from the previous prompt without anyone having tormthe cache directory. Version 2: the thinker gained a generic system prompt (Thinker::SYSTEM_PROMPT), changing the effective prompt for every entry. 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
The easy path is Synthesizer.new(transport: …) — the Thinker and the production on-disk Cache are built right here, so wiring one up needs nothing beyond the transport the host agent already has. thinker: exists as the explicit override (a custom callable, a test fake) and is mutually exclusive with the transport path.
68 69 70 71 72 73 74 75 |
# File 'lib/pikuri/mcp/synthesizer.rb', line 68 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.”
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/pikuri/mcp/synthesizer.rb', line 90 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 |