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 +4 -4
- data/lib/pikuri/mcp/cache.rb +1 -1
- data/lib/pikuri/mcp/client_wrapper.rb +2 -1
- data/lib/pikuri/mcp/extension.rb +20 -43
- data/lib/pikuri/mcp/registry.rb +18 -1
- data/lib/pikuri/mcp/servers.rb +23 -20
- data/lib/pikuri/mcp/synthesizer.rb +42 -16
- data/lib/pikuri/mcp/thinker.rb +81 -0
- data/lib/pikuri/mcp/verifier.rb +46 -11
- data/lib/pikuri-mcp.rb +3 -3
- metadata +6 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8eea96fdc325316f284a0af4b04409190da30a250b183dc260e6bc238631780f
|
|
4
|
+
data.tar.gz: 886eed8a4913860b9571c75f71a5ff91860f9d73d8489b0c64e14e5e034ced16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fafa923f924cfd8a39a8e88776828badd61b551a5c7165c292831b6185972267d2cb420a406e3578b65e443ff8bc2ead11e770f68c2dd12372416beba001500b
|
|
7
|
+
data.tar.gz: cc6dc8b41f9978413dcc1ac81c15a26ea2f3c844be5b58adce4c8246bafbdfccb216f43f8a732d1ebe9429334022e078c23d4b5300bfdd15fb38e0d802145033
|
data/lib/pikuri/mcp/cache.rb
CHANGED
|
@@ -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
|
|
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)
|
data/lib/pikuri/mcp/extension.rb
CHANGED
|
@@ -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#
|
|
20
|
-
# call back into the right chat when the
|
|
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
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
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
|
|
84
|
-
|
|
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
|
|
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
|
|
114
|
+
# @param ctx [Pikuri::Agent::ExtensionContext]
|
|
112
115
|
# @return [void]
|
|
113
|
-
def bind(
|
|
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(
|
|
122
|
-
|
|
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
|
data/lib/pikuri/mcp/registry.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/pikuri/mcp/servers.rb
CHANGED
|
@@ -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#
|
|
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
|
|
190
|
-
#
|
|
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
|
|
195
|
+
# @param ctx [Pikuri::Agent::ExtensionContext]
|
|
195
196
|
# @return [Connect]
|
|
196
|
-
def build_mcp_connect_tool(
|
|
197
|
-
Connect.new(servers: self,
|
|
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
|
|
230
|
-
# underlying +RubyLLM::Chat
|
|
231
|
-
#
|
|
232
|
-
# {
|
|
233
|
-
# {Connect}'
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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
|
|
495
|
-
# are registered.
|
|
496
|
-
def initialize(servers:,
|
|
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,
|
|
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
|
|
15
|
-
# propagates up through {#call} (it's
|
|
16
|
-
# caught by the +StandardError+ rescue) so a
|
|
17
|
-
# during a boot-time synthesis pass aborts
|
|
18
|
-
# rather than being silently logged as a
|
|
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
|
-
|
|
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
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
data/lib/pikuri/mcp/verifier.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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::
|
|
11
|
-
# +Pikuri::Mcp::
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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: []
|