pikuri-mcp 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2940090293173f7028d06eeda7cdfbe0aaca213306acac6cc24d01428a0a69c
4
- data.tar.gz: 8f4a1a4b47e5ff405de3e5b7e35422c0bb2faff0cdee26bd4457d2e35da92cae
3
+ metadata.gz: 8eea96fdc325316f284a0af4b04409190da30a250b183dc260e6bc238631780f
4
+ data.tar.gz: 886eed8a4913860b9571c75f71a5ff91860f9d73d8489b0c64e14e5e034ced16
5
5
  SHA512:
6
- metadata.gz: 4ae9da8ad578c4090f402dd16a76cfc1362450ee0e181b7c4655dd039d11a1a6f1028cb892daeb490c466c88bf9ac138bdc891cd35ccd6f87afb44a433ce7cbd
7
- data.tar.gz: 80620537024513d144db6de8a9e9750d6319d89f02374613a6566f4142e5dc460bc570d61c8ce83d2b26f65b16d84556629f8f70bc8c38e4c610f0f7be79334c
6
+ metadata.gz: fafa923f924cfd8a39a8e88776828badd61b551a5c7165c292831b6185972267d2cb420a406e3578b65e443ff8bc2ead11e770f68c2dd12372416beba001500b
7
+ data.tar.gz: cc6dc8b41f9978413dcc1ac81c15a26ea2f3c844be5b58adce4c8246bafbdfccb216f43f8a732d1ebe9429334022e078c23d4b5300bfdd15fb38e0d802145033
@@ -71,7 +71,7 @@ module Pikuri
71
71
 
72
72
  # Return the cached description for the given +(entry, client,
73
73
  # tools)+ triple if a fresh entry exists, otherwise yield to
74
- # compute it (typically a {Pikuri::Agent.think} call inside
74
+ # compute it (typically a one-shot thinker-agent run inside
75
75
  # {Servers#try_synthesize_description}), persist the result, and
76
76
  # return it.
77
77
  #
@@ -208,7 +208,8 @@ module Pikuri
208
208
  when Registry::StdioEntry
209
209
  MCP::Client::Stdio.new(
210
210
  command: @entry.command.first,
211
- args: @entry.command.drop(1)
211
+ args: @entry.command.drop(1),
212
+ env: @entry.env
212
213
  )
213
214
  when Registry::HttpEntry
214
215
  MCP::Client::HTTP.new(url: @entry.url, headers: @entry.headers)
@@ -16,8 +16,9 @@ module Pikuri
16
16
  # the *shared* {Mcp::Servers} runtime — that's the resource the
17
17
  # extension owns. +bind+ fires once on the parent agent and
18
18
  # installs a +Connect+ tool keyed to it, registered via
19
- # {Agent#internal_add_tool} so the tool's +execute+ closure can
20
- # call back into the right chat when the LLM activates a server.
19
+ # {Pikuri::Agent::ExtensionContext#add_raw_tool} so the tool's
20
+ # +execute+ closure can call back into the right chat when the
21
+ # LLM activates a server.
21
22
  # Sub-agents do not inherit extensions, so they receive neither
22
23
  # the +Connect+ tool nor any MCP-backed tools — personas own
23
24
  # their toolset by construction.
@@ -70,21 +71,23 @@ module Pikuri
70
71
 
71
72
  # Build the shared {Servers} runtime, append the
72
73
  # +<available_mcps>+ block to the system prompt, and register
73
- # the close handler. The +Synthesizer+ / +Verifier+ thinker
74
- # closure captures the Configurator's transport + cancellable
75
- # so the LLM-driven boot passes run against the same model
76
- # the agent itself uses and honor the same cancel flag.
74
+ # the close handler. The {Synthesizer} / {Verifier} are
75
+ # handed the Configurator's transport + cancellable and build
76
+ # their own {Thinker} + production {Cache} from them, so the
77
+ # LLM-driven boot passes run against the same model the agent
78
+ # itself uses and honor the same cancel flag.
77
79
  #
78
80
  # @param c [Pikuri::Agent::Configurator]
79
81
  # @return [void]
80
82
  def configure(c)
81
83
  return if @registry.empty?
82
84
 
83
- if @synthesize_descriptions || @verify_mcp_servers
84
- thinker = build_thinker(c.transport, c.cancellable)
85
+ if @synthesize_descriptions
86
+ synthesizer = Mcp::Synthesizer.new(transport: c.transport, cancellable: c.cancellable)
87
+ end
88
+ if @verify_mcp_servers
89
+ verifier = Mcp::Verifier.new(transport: c.transport, cancellable: c.cancellable)
85
90
  end
86
- synthesizer = build_synthesizer(thinker, c.transport.model) if @synthesize_descriptions
87
- verifier = build_verifier(thinker, c.transport.model) if @verify_mcp_servers
88
91
 
89
92
  # Two-phase on purpose: construct (pure), arm cleanup, then
90
93
  # start. Arming +on_close+ *before* the failure-prone
@@ -103,52 +106,26 @@ module Pikuri
103
106
  end
104
107
 
105
108
  # Register a per-agent +mcp_connect+ tool on the agent's chat.
106
- # The tool's +execute+ closure captures the agent reference so
107
- # activations register their tools on the correct chat — see
109
+ # The tool's +execute+ closure captures the {ExtensionContext}
110
+ # so activations register their tools on the correct chat — see
108
111
  # IDEAS.md §"Two invariants worth recording" for the
109
112
  # static-vs-dynamic tool boundary.
110
113
  #
111
- # @param agent [Pikuri::Agent]
114
+ # @param ctx [Pikuri::Agent::ExtensionContext]
112
115
  # @return [void]
113
- def bind(agent)
116
+ def bind(ctx)
114
117
  return if @servers.nil? || @servers.empty?
115
118
 
116
- if agent.tools.any?(Mcp::Servers::Connect)
119
+ if ctx.agent.tools.any?(Mcp::Servers::Connect)
117
120
  raise 'Mcp::Servers::Connect cannot be passed in tools: when an MCP runtime is wired; ' \
118
121
  'Agent auto-registers it.'
119
122
  end
120
123
 
121
- connect = @servers.build_mcp_connect_tool(agent)
122
- agent.internal_add_tool(connect.to_ruby_llm_tool)
124
+ connect = @servers.build_mcp_connect_tool(ctx)
125
+ ctx.add_raw_tool(connect.to_ruby_llm_tool)
123
126
  nil
124
127
  end
125
128
 
126
- private
127
-
128
- def build_thinker(transport, cancellable)
129
- ->(prompt) { Pikuri::Agent.think(transport: transport, prompt: prompt, cancellable: cancellable) }
130
- end
131
-
132
- def build_synthesizer(thinker, model_id)
133
- Mcp::Synthesizer.new(
134
- thinker: thinker,
135
- cache: Mcp::Cache.new(
136
- model_id: model_id,
137
- prompt_version: Mcp::Synthesizer::PROMPT_VERSION
138
- )
139
- )
140
- end
141
-
142
- def build_verifier(thinker, model_id)
143
- Mcp::Verifier.new(
144
- thinker: thinker,
145
- cache: Mcp::Cache.new(
146
- model_id: model_id,
147
- prompt_version: Mcp::Verifier::PROMPT_VERSION,
148
- dir: File.join(File.dirname(Mcp::Cache::DIR), 'mcp_verifications')
149
- )
150
- )
151
- end
152
129
  end
153
130
  end
154
131
  end
@@ -43,13 +43,30 @@ module Pikuri
43
43
  # owns the actual spawn — see CLAUDE.md "Subprocess seam" carve-out.
44
44
  # First element is the executable; remaining elements are arguments.
45
45
  #
46
+ # +env+ carries extra environment variables for the spawned process —
47
+ # the stdio counterpart of {HttpEntry}'s +headers:+, and the right
48
+ # place for a server's auth token (e.g. +{"GITHUB_TOKEN" => "..."}+)
49
+ # rather than baking secrets into the argv. The +mcp+ gem merges it
50
+ # over the inherited environment; an empty hash (the default) changes
51
+ # nothing. Like {HttpEntry}'s +headers+, +env+ is excluded from the
52
+ # cache fingerprint ({Cache#transport_descriptor}) so secrets never
53
+ # land on disk.
54
+ #
46
55
  # @!attribute [r] id
47
56
  # @return [String] short identifier the LLM uses to refer to this
48
57
  # server, e.g. +"gmail"+ or +"maven-tools"+.
49
58
  # @!attribute [r] command
50
59
  # @return [Array<String>] argv to spawn the server,
51
60
  # e.g. +["npx", "mcp-maven-deps"]+.
52
- StdioEntry = Data.define(:id, :command)
61
+ # @!attribute [r] env
62
+ # @return [Hash{String => String}] extra environment variables for
63
+ # the spawned process, merged over the inherited environment.
64
+ # Frozen. Defaults to an empty hash.
65
+ StdioEntry = Data.define(:id, :command, :env) do
66
+ def initialize(id:, command:, env: {})
67
+ super(id: id, command: command, env: env.freeze)
68
+ end
69
+ end
53
70
 
54
71
  # One remote-HTTP MCP server's configuration: a unique id, the
55
72
  # endpoint URL, and optional headers (typically auth) sent on every
@@ -85,8 +85,8 @@ module Pikuri
85
85
  # MCP tools arrive with JSON Schema in their +input_schema+;
86
86
  # +RubyLLM::Tool.params(schema)+ accepts that shape directly. We
87
87
  # therefore synthesize +RubyLLM::Tool+ subclasses inside this class
88
- # and feed them through {Agent#internal_add_tool} — no
89
- # +Pikuri::Tool::Parameters+ in the middle. The strict-validation
88
+ # and feed them through {Pikuri::Agent::ExtensionContext#add_raw_tool}
89
+ # — no +Pikuri::Tool::Parameters+ in the middle. The strict-validation
90
90
  # contract pikuri commits to for native tools is intentionally not
91
91
  # extended to MCP tools in v1; MCP-side validation catches bad input,
92
92
  # and the provenance prefix + audit log are the compensating
@@ -186,15 +186,16 @@ module Pikuri
186
186
  @live_ids.empty?
187
187
  end
188
188
 
189
- # Build the +mcp_connect+ tool bound to the given agent. The agent
190
- # passes itself so the tool can call +agent.internal_add_tool+
189
+ # Build the +mcp_connect+ tool bound to the given agent, via its
190
+ # {Pikuri::Agent::ExtensionContext}. {Extension#bind} passes the
191
+ # context it received so the tool can call +ctx.add_raw_tool+
191
192
  # without a circular constructor dance. Each call returns a fresh
192
193
  # {Connect} with an empty activation set.
193
194
  #
194
- # @param agent [Agent]
195
+ # @param ctx [Pikuri::Agent::ExtensionContext]
195
196
  # @return [Connect]
196
- def build_mcp_connect_tool(agent)
197
- Connect.new(servers: self, agent: agent)
197
+ def build_mcp_connect_tool(ctx)
198
+ Connect.new(servers: self, ctx: ctx)
198
199
  end
199
200
 
200
201
  # System-prompt block advertising every live MCP server to the LLM.
@@ -226,24 +227,25 @@ module Pikuri
226
227
  lines.join("\n")
227
228
  end
228
229
 
229
- # Register every tool exposed by server +id+ into +agent+'s
230
- # underlying +RubyLLM::Chat+. Public so {View#register_tools_with_agent}
231
- # can delegate; intended caller is {Connect} via
232
- # {Agent#internal_add_tool}. Doesn't track activation — that's
233
- # {Connect}'s job (each agent's tool instance owns its own set).
230
+ # Register every tool exposed by server +id+ into an agent's
231
+ # underlying +RubyLLM::Chat+, through the agent's
232
+ # {Pikuri::Agent::ExtensionContext}. Public so
233
+ # {View#register_tools_with_agent} can delegate; intended caller
234
+ # is {Connect}. Doesn't track activation — that's {Connect}'s
235
+ # job (each agent's tool instance owns its own set).
234
236
  #
235
237
  # @param id [String] server id; must be in {#live_ids}.
236
- # @param agent [Agent]
238
+ # @param ctx [Pikuri::Agent::ExtensionContext]
237
239
  # @return [Integer] number of tools registered.
238
240
  # @raise [ArgumentError] if +id+ isn't live.
239
- def register_tools_with_agent(id, agent)
241
+ def register_tools_with_agent(id, ctx)
240
242
  raise ArgumentError, "MCP server #{id.inspect} is not live" unless @live_ids.include?(id)
241
243
 
242
244
  wrapper = @wrappers.fetch(id)
243
245
  tools = @tools_cache.fetch(id)
244
246
  tools.each do |mcp_tool|
245
247
  rb_tool = synthesize_ruby_llm_tool(server_id: id, wrapper: wrapper, mcp_tool: mcp_tool)
246
- agent.internal_add_tool(rb_tool)
248
+ ctx.add_raw_tool(rb_tool)
247
249
  end
248
250
  tools.size
249
251
  end
@@ -470,7 +472,8 @@ module Pikuri
470
472
 
471
473
  # The +mcp_connect+ tool exposed to the LLM. Private inner class —
472
474
  # instantiated only by {Servers#build_mcp_connect_tool} (called
473
- # from {Agent#initialize}), never by application code.
475
+ # from {Extension#bind} with the agent's
476
+ # {Pikuri::Agent::ExtensionContext}), never by application code.
474
477
  #
475
478
  # Tracks its own activation +Set+ so re-connecting an already-active
476
479
  # server returns a recoverable +"Error: ..."+ String (the LLM has
@@ -491,9 +494,9 @@ module Pikuri
491
494
 
492
495
  # @param servers [Servers] shared runtime this tool resolves
493
496
  # ids against and registers tools through.
494
- # @param agent [Agent] agent into whose chat the activated tools
495
- # are registered.
496
- def initialize(servers:, agent:)
497
+ # @param ctx [Pikuri::Agent::ExtensionContext] context of the
498
+ # agent into whose chat the activated tools are registered.
499
+ def initialize(servers:, ctx:)
497
500
  activated = Set.new
498
501
 
499
502
  super(
@@ -515,7 +518,7 @@ module Pikuri
515
518
  next "Error: server '#{id}' is already connected; its tools are in your toolset, call them directly."
516
519
  end
517
520
 
518
- count = servers.register_tools_with_agent(id, agent)
521
+ count = servers.register_tools_with_agent(id, ctx)
519
522
  activated << id
520
523
  "Connected to MCP server '#{id}'. Added #{count} tool#{'s' if count != 1} to your toolset."
521
524
  }
@@ -11,11 +11,12 @@ module Pikuri
11
11
  #
12
12
  # == Cancellation
13
13
  #
14
- # +Cancellable::Cancelled+ raised from inside the +@thinker+
15
- # propagates up through {#call} (it's specifically *not*
16
- # caught by the +StandardError+ rescue) so a user's Ctrl+C
17
- # during a boot-time synthesis pass aborts startup cleanly
18
- # rather than being silently logged as a "synthesis failure."
14
+ # +Cancellable::Cancelled+ raised from inside the thinker (the
15
+ # {Thinker} pre-call check) propagates up through {#call} (it's
16
+ # specifically *not* caught by the +StandardError+ rescue) so a
17
+ # user's Ctrl+C during a boot-time synthesis pass aborts
18
+ # startup cleanly rather than being silently logged as a
19
+ # "synthesis failure."
19
20
  #
20
21
  # == Fail-soft on errors
21
22
  #
@@ -34,18 +35,43 @@ module Pikuri
34
35
  # initializer kwarg, so a bump invalidates every cached entry
35
36
  # from the previous prompt without anyone having to +rm+ the
36
37
  # cache directory.
37
- PROMPT_VERSION = 1
38
+ # Version 2: the thinker gained a generic system prompt
39
+ # ({Thinker::SYSTEM_PROMPT}), changing the effective prompt
40
+ # for every entry.
41
+ PROMPT_VERSION = 2
38
42
 
39
- # @param thinker [Proc] one-arg callable invoked as
40
- # +thinker.call(prompt)+ typically a closure over
41
- # {Pikuri::Agent.think} that captures the agent's transport
42
- # and cancellable.
43
- # @param cache [Cache, Cache::NULL] storage layer. Defaults to
44
- # {Cache::NULL} so tests can construct a synthesizer without
45
- # a real cache.
46
- def initialize(thinker:, cache: Cache::NULL)
47
- @thinker = thinker
48
- @cache = cache
43
+ # The easy path is +Synthesizer.new(transport: ...)+ — the
44
+ # {Thinker} and the production on-disk {Cache} are built
45
+ # right here, so wiring one up needs nothing beyond the
46
+ # transport the host agent already has. +thinker:+ exists as
47
+ # the explicit override (a custom callable, a test fake) and
48
+ # is mutually exclusive with the transport path.
49
+ #
50
+ # @param transport [Pikuri::Agent::ChatTransport, nil] when
51
+ # set, a {Thinker} is constructed from it (and
52
+ # +cancellable:+); the synthesis passes run against this
53
+ # model.
54
+ # @param cancellable [Pikuri::Agent::Control::Cancellable, nil]
55
+ # forwarded to the constructed {Thinker} so a boot-time
56
+ # Ctrl+C aborts the pass. Only meaningful on the
57
+ # +transport:+ path.
58
+ # @param thinker [#call, nil] one-arg callable invoked as
59
+ # +thinker.call(prompt)+, replacing the built-in {Thinker}.
60
+ # @param cache [Cache, Cache::NULL, nil] storage layer. When
61
+ # +nil+ (the default): the +transport:+ path builds the
62
+ # production on-disk {Cache} keyed on the transport's model
63
+ # and {PROMPT_VERSION}; the +thinker:+ path falls back to
64
+ # {Cache::NULL} (no persistence — the test default).
65
+ # @raise [ArgumentError] when neither or both of +transport:+
66
+ # and +thinker:+ are given, or when +cancellable:+ is
67
+ # combined with +thinker:+
68
+ def initialize(transport: nil, cancellable: nil, thinker: nil, cache: nil)
69
+ raise ArgumentError, 'pass exactly one of transport: or thinker:' if transport.nil? == thinker.nil?
70
+ raise ArgumentError, 'cancellable: only applies to the transport: path' if thinker && cancellable
71
+
72
+ @thinker = thinker || Thinker.new(transport: transport, cancellable: cancellable)
73
+ @cache = cache ||
74
+ (transport ? Cache.new(model_id: transport.model, prompt_version: PROMPT_VERSION) : Cache::NULL)
49
75
  end
50
76
 
51
77
  # Produce the description for one server. Returns the cleaned
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Mcp
5
+ # One-shot LLM pass for the MCP boot-time checks: {Synthesizer}
6
+ # (description summarization) and {Verifier} (injection check)
7
+ # call {#call} with a self-contained prompt and get the model's
8
+ # reply back as a plain String. Each call constructs a fresh
9
+ # tools-free {Pikuri::Agent}, runs the prompt as its single
10
+ # turn, and closes it — fresh per call deliberately, so
11
+ # per-server prompts don't accumulate in one shared chat
12
+ # history and cross-contaminate. The construction cost is noise
13
+ # next to the LLM round-trip, and {Cache} short-circuits before
14
+ # {#call} fires anyway.
15
+ #
16
+ # No listeners (boot passes are silent), no step limit (no
17
+ # tools are wired, so the model has nothing to tick).
18
+ #
19
+ # == Cancellation
20
+ #
21
+ # +cancellable+ is checked once before each call — the "gentle
22
+ # cancel" semantic: a flag flipped between calls raises
23
+ # promptly; the in-flight HTTP call itself is not interrupted.
24
+ # It is deliberately NOT passed into the one-shot agent:
25
+ # +Agent#run_loop+ resets its cancellable at the turn boundary,
26
+ # which would erase a pending Ctrl+C instead of honoring it
27
+ # (and a tools-free non-streaming turn has no check points
28
+ # anyway).
29
+ #
30
+ # == Failure
31
+ #
32
+ # Errors from the provider propagate to the caller verbatim —
33
+ # no recovery layer here. {Synthesizer} rescues at its level
34
+ # and falls back to a default; {Verifier} lets them bubble.
35
+ class Thinker
36
+ # System prompt for the one-shot agents. The real
37
+ # instructions (synthesis framing, verification rubric) live
38
+ # in the prompt {Synthesizer} / {Verifier} build and send as
39
+ # the user turn, so this stays generic.
40
+ SYSTEM_PROMPT =
41
+ 'You are a one-shot analysis pass inside an agent boot sequence. ' \
42
+ 'Follow the instructions in the user message exactly and reply ' \
43
+ 'with only what they ask for — no preamble.'
44
+
45
+ # @param transport [Pikuri::Agent::ChatTransport] the
46
+ # model-resolution triple every one-shot agent is
47
+ # constructed with — typically the host agent's own, so
48
+ # boot passes run against the same model.
49
+ # @param cancellable [Pikuri::Agent::Control::Cancellable, nil]
50
+ # when set, checked before each {#call} so a boot-time
51
+ # Ctrl+C aborts startup on the next pass.
52
+ # @raise [ArgumentError] when +transport+ is +nil+
53
+ def initialize(transport:, cancellable: nil)
54
+ raise ArgumentError, 'transport must not be nil' if transport.nil?
55
+
56
+ @transport = transport
57
+ @cancellable = cancellable
58
+ end
59
+
60
+ # Run one prompt through a fresh tools-free agent.
61
+ #
62
+ # @param prompt [String] self-contained instructions + data,
63
+ # sent as the single user turn
64
+ # @return [String] the assistant's reply content (+""+ when
65
+ # the provider produced no assistant message)
66
+ # @raise [Pikuri::Agent::Control::Cancellable::Cancelled]
67
+ # when the +cancellable+ flag was tripped at the pre-call
68
+ # check
69
+ def call(prompt)
70
+ @cancellable&.check!
71
+ agent = Pikuri::Agent.new(transport: @transport, system_prompt: SYSTEM_PROMPT)
72
+ begin
73
+ agent.run_loop(user_message: prompt)
74
+ agent.last_assistant_content.to_s
75
+ ensure
76
+ agent.close
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -62,7 +62,16 @@ module Pikuri
62
62
  # folds this into its key fingerprint so a prompt edit
63
63
  # invalidates every cached "OK" without anyone having to
64
64
  # +rm+ the cache directory.
65
- PROMPT_VERSION = 1
65
+ # Version 2: the thinker gained a generic system prompt
66
+ # ({Thinker::SYSTEM_PROMPT}), changing the effective prompt
67
+ # for every entry.
68
+ PROMPT_VERSION = 2
69
+
70
+ # Where the production cache lives — a sibling of
71
+ # {Cache::DIR} so verification verdicts never collide with
72
+ # synthesized descriptions (same key fingerprint, different
73
+ # meaning).
74
+ CACHE_DIR = File.join(File.dirname(Cache::DIR), 'mcp_verifications')
66
75
 
67
76
  # Code points with no legitimate place in human-readable tool
68
77
  # documentation. Each range catches one category of "hide
@@ -86,16 +95,42 @@ module Pikuri
86
95
  # describes WHAT was found and WHERE.
87
96
  class InjectionDetected < StandardError; end
88
97
 
89
- # @param thinker [Proc] one-arg callable invoked as
90
- # +thinker.call(prompt)+; same closure shape {Synthesizer}
91
- # uses.
92
- # @param cache [Cache, Cache::NULL] cache for the LLM-pass
93
- # result. Defaults to {Cache::NULL} so tests can skip
94
- # disk persistence; production wiring in {Agent} uses a
95
- # real {Cache} pointing at a +mcp_verifications+ dir.
96
- def initialize(thinker:, cache: Cache::NULL)
97
- @thinker = thinker
98
- @cache = cache
98
+ # The easy path is +Verifier.new(transport: ...)+ — the
99
+ # {Thinker} and the production on-disk {Cache} (under
100
+ # {CACHE_DIR}) are built right here. +thinker:+ exists as the
101
+ # explicit override (a custom callable, a test fake) and is
102
+ # mutually exclusive with the transport path. Same
103
+ # constructor shape as {Synthesizer}.
104
+ #
105
+ # @param transport [Pikuri::Agent::ChatTransport, nil] when
106
+ # set, a {Thinker} is constructed from it (and
107
+ # +cancellable:+); the verification passes run against this
108
+ # model.
109
+ # @param cancellable [Pikuri::Agent::Control::Cancellable, nil]
110
+ # forwarded to the constructed {Thinker} so a boot-time
111
+ # Ctrl+C aborts the pass. Only meaningful on the
112
+ # +transport:+ path.
113
+ # @param thinker [#call, nil] one-arg callable invoked as
114
+ # +thinker.call(prompt)+, replacing the built-in {Thinker}.
115
+ # @param cache [Cache, Cache::NULL, nil] cache for the
116
+ # LLM-pass result. When +nil+ (the default): the
117
+ # +transport:+ path builds the production on-disk {Cache}
118
+ # under {CACHE_DIR}; the +thinker:+ path falls back to
119
+ # {Cache::NULL} (no persistence — the test default).
120
+ # @raise [ArgumentError] when neither or both of +transport:+
121
+ # and +thinker:+ are given, or when +cancellable:+ is
122
+ # combined with +thinker:+
123
+ def initialize(transport: nil, cancellable: nil, thinker: nil, cache: nil)
124
+ raise ArgumentError, 'pass exactly one of transport: or thinker:' if transport.nil? == thinker.nil?
125
+ raise ArgumentError, 'cancellable: only applies to the transport: path' if thinker && cancellable
126
+
127
+ @thinker = thinker || Thinker.new(transport: transport, cancellable: cancellable)
128
+ @cache = cache ||
129
+ if transport
130
+ Cache.new(model_id: transport.model, prompt_version: PROMPT_VERSION, dir: CACHE_DIR)
131
+ else
132
+ Cache::NULL
133
+ end
99
134
  end
100
135
 
101
136
  # Verify the server's surface. Returns nothing on success;
data/lib/pikuri-mcp.rb CHANGED
@@ -7,9 +7,9 @@ require 'pikuri-core'
7
7
  # +Pikuri::+ namespace alongside pikuri-core. After
8
8
  # +require 'pikuri-mcp'+, +Pikuri::Mcp::Registry+,
9
9
  # +Pikuri::Mcp::Servers+, +Pikuri::Mcp::Synthesizer+,
10
- # +Pikuri::Mcp::Verifier+, +Pikuri::Mcp::Cache+,
11
- # +Pikuri::Mcp::ClientWrapper+ and +Pikuri::Mcp::Extension+ are all
12
- # defined.
10
+ # +Pikuri::Mcp::Verifier+, +Pikuri::Mcp::Thinker+,
11
+ # +Pikuri::Mcp::Cache+, +Pikuri::Mcp::ClientWrapper+ and
12
+ # +Pikuri::Mcp::Extension+ are all defined.
13
13
  #
14
14
  # Per-gem loader (not shared with pikuri-core's loader) so each gem
15
15
  # owns its own +lib/+ tree and the cooperation between gems is via
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pikuri-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Vysny
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: pikuri-core
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - '='
18
17
  - !ruby/object:Gem::Version
19
- version: 0.0.6
18
+ version: 0.0.7
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - '='
25
24
  - !ruby/object:Gem::Version
26
- version: 0.0.6
25
+ version: 0.0.7
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: mcp
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -62,6 +61,7 @@ files:
62
61
  - lib/pikuri/mcp/registry.rb
63
62
  - lib/pikuri/mcp/servers.rb
64
63
  - lib/pikuri/mcp/synthesizer.rb
64
+ - lib/pikuri/mcp/thinker.rb
65
65
  - lib/pikuri/mcp/verifier.rb
66
66
  - prompts/pikuri-openlibrary.txt
67
67
  homepage: https://codeberg.org/mvysny/pikuri
@@ -72,7 +72,6 @@ metadata:
72
72
  changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
73
73
  bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
74
74
  rubygems_mfa_required: 'true'
75
- post_install_message:
76
75
  rdoc_options: []
77
76
  require_paths:
78
77
  - lib
@@ -87,8 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
86
  - !ruby/object:Gem::Version
88
87
  version: '0'
89
88
  requirements: []
90
- rubygems_version: 3.5.22
91
- signing_key:
89
+ rubygems_version: 3.6.7
92
90
  specification_version: 4
93
91
  summary: Model Context Protocol (MCP) support for pikuri.
94
92
  test_files: []