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. 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

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 here; thinker: is the explicit override, mutually exclusive with transport:.

Parameters:

  • transport (Pikuri::Agent::ChatTransport, nil) (defaults to: nil)

    builds a Thinker (with cancellable:); the passes run against this model.

  • cancellable (Pikuri::Agent::Control::Cancellable, nil) (defaults to: nil)

    forwarded to the Thinker so a boot-time Ctrl+C aborts. Transport path only.

  • thinker (#call, nil) (defaults to: nil)

    thinker.call(prompt), replacing the built-in Thinker.

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

    nil builds the on-disk Cache on the transport: path, Cache::NULL on the thinker: path.

Raises:

  • (ArgumentError)

    when neither or both of +transport:+/+thinker:+ are given, or cancellable: is combined with thinker:.



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."

Parameters:

Returns:

  • (String, nil)

Raises:

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

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



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