Class: Pikuri::Mcp::Synthesizer

Inherits:
Object
  • Object
show all
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 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 to rm the cache directory.

1

Instance Method Summary collapse

Constructor Details

#initialize(thinker:, cache: Cache::NULL) ⇒ Synthesizer

Returns a new instance of Synthesizer.

Parameters:

  • thinker (Proc)

    one-arg callable invoked as thinker.call(prompt) — typically a closure over Agent.think that captures the agent’s transport and cancellable.

  • cache (Cache, Cache::NULL) (defaults to: Cache::NULL)

    storage layer. Defaults to Cache::NULL so tests can construct a synthesizer without a real cache.



46
47
48
49
# File 'lib/pikuri/mcp/synthesizer.rb', line 46

def initialize(thinker:, cache: Cache::NULL)
  @thinker = thinker
  @cache = cache
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.”

Parameters:

Returns:

  • (String, nil)

Raises:

  • (Pikuri::Agent::Control::Cancellable::Cancelled)

    when the underlying thinker raises Cancelled — propagated so boot-time Ctrl+C aborts startup.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/pikuri/mcp/synthesizer.rb', line 64

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.message}); falling back to serverInfo.name."
  )
  nil
end