riffer 0.26.0 → 0.27.0
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/.agents/architecture.md +35 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +12 -0
- data/README.md +1 -0
- data/docs/03_AGENTS.md +4 -0
- data/docs/10_CONFIGURATION.md +24 -5
- data/docs/14_MCP.md +144 -0
- data/lib/riffer/agent.rb +80 -1
- data/lib/riffer/config.rb +13 -0
- data/lib/riffer/mcp/authenticated_tool.rb +65 -0
- data/lib/riffer/mcp/client.rb +67 -0
- data/lib/riffer/mcp/manifest.rb +45 -0
- data/lib/riffer/mcp/registration.rb +76 -0
- data/lib/riffer/mcp/registry.rb +62 -0
- data/lib/riffer/mcp/tool_factory.rb +54 -0
- data/lib/riffer/mcp.rb +57 -0
- data/lib/riffer/providers/anthropic.rb +41 -36
- data/lib/riffer/providers/open_ai.rb +33 -26
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent.rbs +33 -0
- data/sig/generated/riffer/config.rbs +20 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +17 -0
- data/sig/generated/riffer/mcp/client.rbs +33 -0
- data/sig/generated/riffer/mcp/manifest.rbs +30 -0
- data/sig/generated/riffer/mcp/registration.rbs +49 -0
- data/sig/generated/riffer/mcp/registry.rbs +35 -0
- data/sig/generated/riffer/mcp/tool_factory.rbs +25 -0
- data/sig/generated/riffer/mcp.rbs +51 -0
- data/sig/generated/riffer/providers/anthropic.rbs +2 -2
- metadata +44 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Per-server state managed by Riffer::Mcp::Registry.
|
|
5
|
+
#
|
|
6
|
+
# Created when a server is registered. Discovers tools via the MCP
|
|
7
|
+
# +tools/list+ call, then generates tool classes.
|
|
8
|
+
#
|
|
9
|
+
class Riffer::Mcp::Registration
|
|
10
|
+
# The manifest that describes this server.
|
|
11
|
+
attr_reader :manifest #: Riffer::Mcp::Manifest
|
|
12
|
+
|
|
13
|
+
# Generated Riffer::Tool subclasses.
|
|
14
|
+
#
|
|
15
|
+
#--
|
|
16
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
17
|
+
def tools
|
|
18
|
+
@mutex.synchronize { @tools }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#--
|
|
22
|
+
#: (Riffer::Mcp::Manifest) -> void
|
|
23
|
+
def initialize(manifest)
|
|
24
|
+
@manifest = manifest
|
|
25
|
+
@cancelled = false
|
|
26
|
+
@tools = []
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
run_discovery
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Retires this registration, preventing in-flight discovery from publishing
|
|
32
|
+
# state.
|
|
33
|
+
#
|
|
34
|
+
#--
|
|
35
|
+
#: () -> void
|
|
36
|
+
def retire!
|
|
37
|
+
@mutex.synchronize { @cancelled = true }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns true if this registration has been retired.
|
|
41
|
+
#
|
|
42
|
+
#--
|
|
43
|
+
#: () -> bool
|
|
44
|
+
def retired?
|
|
45
|
+
@mutex.synchronize { @cancelled }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Runs tool discovery using the configured Runner.
|
|
51
|
+
#
|
|
52
|
+
# With +Runner::Sequential+ (default) discovery blocks inline. With
|
|
53
|
+
# +Runner::Threaded+ discovery runs on a pool thread but +map+ still
|
|
54
|
+
# blocks the caller — useful for Rails connection-pool isolation.
|
|
55
|
+
#
|
|
56
|
+
#--
|
|
57
|
+
#: () -> void
|
|
58
|
+
def run_discovery
|
|
59
|
+
Riffer.config.mcp.discovery_runner.map([nil], context: nil) do |_|
|
|
60
|
+
client = build_client
|
|
61
|
+
tool_defs = client.tools_list
|
|
62
|
+
tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
|
|
63
|
+
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
next if @cancelled
|
|
66
|
+
@tools = tools.freeze
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#--
|
|
72
|
+
#: () -> Riffer::Mcp::Client
|
|
73
|
+
def build_client
|
|
74
|
+
Riffer::Mcp::Client.new(endpoint: @manifest.endpoint, headers: @manifest.discovery_headers || {})
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Thread-safe global store for MCP server registrations.
|
|
5
|
+
#
|
|
6
|
+
# Keyed by manifest name. All public methods are mutex-guarded.
|
|
7
|
+
#
|
|
8
|
+
module Riffer::Mcp::Registry
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@store = {} #: Hash[String, Riffer::Mcp::Registration]
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Registers an MCP server and starts async tool discovery.
|
|
14
|
+
#
|
|
15
|
+
# Accepts a Manifest instance or a hash of manifest keyword arguments.
|
|
16
|
+
# Replaces any existing registration with the same name.
|
|
17
|
+
#
|
|
18
|
+
#--
|
|
19
|
+
#: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
|
|
20
|
+
def register(manifest_or_hash)
|
|
21
|
+
manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash)
|
|
22
|
+
registration = Riffer::Mcp::Registration.new(manifest)
|
|
23
|
+
old = @mutex.synchronize do
|
|
24
|
+
previous = @store[manifest.name]
|
|
25
|
+
@store[manifest.name] = registration
|
|
26
|
+
previous
|
|
27
|
+
end
|
|
28
|
+
old&.retire!
|
|
29
|
+
registration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Removes a registration by name.
|
|
33
|
+
#
|
|
34
|
+
#--
|
|
35
|
+
#: ((String | Symbol)) -> void
|
|
36
|
+
def unregister(name)
|
|
37
|
+
removed = @mutex.synchronize { @store.delete(name.to_s) }
|
|
38
|
+
removed&.retire!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns a frozen snapshot of all current registrations.
|
|
42
|
+
#
|
|
43
|
+
#--
|
|
44
|
+
#: () -> Hash[String, Riffer::Mcp::Registration]
|
|
45
|
+
def registrations
|
|
46
|
+
@mutex.synchronize { @store.dup.freeze }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns all registrations whose manifest tags intersect with the given tags.
|
|
50
|
+
#
|
|
51
|
+
# Tags are normalized to symbols before matching.
|
|
52
|
+
#
|
|
53
|
+
#--
|
|
54
|
+
#: (Array[Symbol]) -> Array[Riffer::Mcp::Registration]
|
|
55
|
+
def find_by_tags(tags)
|
|
56
|
+
normalized = tags.map(&:to_sym)
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
@store.values.select { |reg| (reg.manifest.tags & normalized).any? }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Generates anonymous Riffer::Tool subclasses from MCP tool definitions.
|
|
5
|
+
#
|
|
6
|
+
# Each generated class:
|
|
7
|
+
# - Has +.name+, +.description+, and +.parameters_schema+ derived from the MCP tool definition.
|
|
8
|
+
# - Delegates +#call+ to the MCP client's +tools_call+ method.
|
|
9
|
+
# - Skips Riffer's param validation — the MCP server validates inputs.
|
|
10
|
+
#
|
|
11
|
+
module Riffer::Mcp::ToolFactory
|
|
12
|
+
# Builds one Riffer::Tool subclass per tool definition.
|
|
13
|
+
#
|
|
14
|
+
# Tool names are prefixed with the manifest name to avoid collisions
|
|
15
|
+
# across MCP servers (e.g. +"jira__search"+). The original server-side
|
|
16
|
+
# name is available via +.mcp_server_tool_name+.
|
|
17
|
+
#
|
|
18
|
+
#--
|
|
19
|
+
#: (String, Riffer::Mcp::Client, Array[Hash[Symbol, untyped]]) -> Array[singleton(Riffer::Tool)]
|
|
20
|
+
def self.build(manifest_name, client, tool_defs)
|
|
21
|
+
tool_defs.map { |td| build_tool_class(manifest_name, client, td) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Replaces characters that are unsafe in LLM tool names.
|
|
25
|
+
#: (String) -> String
|
|
26
|
+
def self.sanitize_name_component(str)
|
|
27
|
+
str.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
28
|
+
end
|
|
29
|
+
private_class_method :sanitize_name_component
|
|
30
|
+
|
|
31
|
+
private_class_method def self.build_tool_class(manifest_name, client, td)
|
|
32
|
+
prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
|
|
33
|
+
|
|
34
|
+
Class.new(Riffer::Tool) do
|
|
35
|
+
@mcp_client = client
|
|
36
|
+
@mcp_server_tool_name = td[:name]
|
|
37
|
+
# Set @identifier directly so .identifier does not fall back to
|
|
38
|
+
# class_name_to_path(nil) on this anonymous class.
|
|
39
|
+
@identifier = prefixed
|
|
40
|
+
|
|
41
|
+
define_singleton_method(:name) { prefixed }
|
|
42
|
+
define_singleton_method(:mcp_server_tool_name) { td[:name] }
|
|
43
|
+
define_singleton_method(:description) { td[:description] }
|
|
44
|
+
define_singleton_method(:parameters_schema) { |strict: false| td[:input_schema] || Riffer::Tool.send(:empty_schema) }
|
|
45
|
+
|
|
46
|
+
define_method(:call) do |context:, **kwargs|
|
|
47
|
+
result = self.class.instance_variable_get(:@mcp_client).tools_call(
|
|
48
|
+
self.class.instance_variable_get(:@mcp_server_tool_name), kwargs
|
|
49
|
+
)
|
|
50
|
+
text(result)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/riffer/mcp.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Riffer::Mcp provides integration with Model Context Protocol (MCP) servers.
|
|
5
|
+
#
|
|
6
|
+
# Register MCP servers globally; agents opt-in by tag via the +use_mcp+ DSL.
|
|
7
|
+
# Tags are application-defined; see +docs/14_MCP.md+ (Tags section).
|
|
8
|
+
#
|
|
9
|
+
# Riffer::Mcp.register(
|
|
10
|
+
# name: "github",
|
|
11
|
+
# tags: [:github],
|
|
12
|
+
# endpoint: "https://mcp.github.com",
|
|
13
|
+
# discovery_headers: -> { {"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}"} }
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# class MyAgent < Riffer::Agent
|
|
17
|
+
# model "openai/gpt-4o"
|
|
18
|
+
# use_mcp :github
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module Riffer::Mcp
|
|
22
|
+
# Base error for all MCP-related failures.
|
|
23
|
+
class Error < Riffer::Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when +Riffer.config.mcp.credentials+ returns +nil+ during +tools/call+
|
|
26
|
+
# after the server's tools were included for this run.
|
|
27
|
+
class CredentialsDeniedError < Error; end
|
|
28
|
+
|
|
29
|
+
# Registers an MCP server, blocking until tool discovery completes.
|
|
30
|
+
#
|
|
31
|
+
# Raises on discovery failure. Pass a +Manifest+ instance or a hash with
|
|
32
|
+
# the same keys.
|
|
33
|
+
#
|
|
34
|
+
#--
|
|
35
|
+
#: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
|
|
36
|
+
def self.register(manifest_or_hash)
|
|
37
|
+
Registry.register(manifest_or_hash)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Removes a registration by name.
|
|
41
|
+
#
|
|
42
|
+
# Subsequent agent runs will not see tools from this server.
|
|
43
|
+
#
|
|
44
|
+
#--
|
|
45
|
+
#: (String) -> void
|
|
46
|
+
def self.unregister(name)
|
|
47
|
+
Registry.unregister(name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns all current registrations keyed by name (for introspection).
|
|
51
|
+
#
|
|
52
|
+
#--
|
|
53
|
+
#: () -> Hash[String, Riffer::Mcp::Registration]
|
|
54
|
+
def self.registrations
|
|
55
|
+
Registry.registrations
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -150,30 +150,36 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
150
150
|
**params,
|
|
151
151
|
request_options: {extra_headers: {"accept-encoding" => "identity"}}
|
|
152
152
|
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
stream.each do |event|
|
|
156
|
+
case event
|
|
157
|
+
when Anthropic::Models::RawContentBlockStartEvent
|
|
158
|
+
handle_raw_content_block_start(event, state: current_state)
|
|
159
|
+
when Anthropic::Models::RawContentBlockDeltaEvent
|
|
160
|
+
handle_raw_content_block_delta(event, state: current_state)
|
|
161
|
+
when Anthropic::Streaming::TextEvent
|
|
162
|
+
handle_text_event(event, state: current_state, yielder: yielder)
|
|
163
|
+
when Anthropic::Streaming::ThinkingEvent
|
|
164
|
+
handle_thinking_event(event, state: current_state, yielder: yielder)
|
|
165
|
+
when Anthropic::Streaming::InputJsonEvent
|
|
166
|
+
handle_input_json_event(event, state: current_state, yielder: yielder)
|
|
167
|
+
when Anthropic::Streaming::ContentBlockStopEvent
|
|
168
|
+
block_type = event.content_block&.type.to_s
|
|
169
|
+
handle_content_block_stop_text(event, state: current_state, yielder: yielder) if block_type == "text" && current_state[:text]
|
|
170
|
+
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if block_type == "tool_use"
|
|
171
|
+
handle_content_block_stop_thinking(event, state: current_state, yielder: yielder) if block_type == "thinking" && current_state[:reasoning]
|
|
172
|
+
handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder) if block_type == "server_tool_use"
|
|
173
|
+
handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder) if block_type == "web_search_tool_result"
|
|
174
|
+
when Anthropic::Streaming::MessageStopEvent
|
|
175
|
+
handle_message_stop(event, accumulated_message: stream.accumulated_message, yielder: yielder)
|
|
176
|
+
end
|
|
176
177
|
end
|
|
178
|
+
ensure
|
|
179
|
+
# Anthropic SDK does not auto-close the underlying HTTP stream when
|
|
180
|
+
# iteration is interrupted (raise / fiber cancellation), so the SSE
|
|
181
|
+
# socket leaks until GC. close is idempotent and a no-op after EOF.
|
|
182
|
+
stream.close
|
|
177
183
|
end
|
|
178
184
|
end
|
|
179
185
|
|
|
@@ -280,20 +286,19 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
280
286
|
end
|
|
281
287
|
|
|
282
288
|
#--
|
|
283
|
-
#: (untyped,
|
|
284
|
-
def handle_message_stop(_event,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
289
|
+
#: (untyped, accumulated_message: Anthropic::Models::Message?, yielder: Enumerator::Yielder) -> void
|
|
290
|
+
def handle_message_stop(_event, accumulated_message:, yielder:)
|
|
291
|
+
usage = accumulated_message&.usage
|
|
292
|
+
return unless usage
|
|
293
|
+
|
|
294
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
295
|
+
token_usage: Riffer::TokenUsage.new(
|
|
296
|
+
input_tokens: usage.input_tokens,
|
|
297
|
+
output_tokens: usage.output_tokens,
|
|
298
|
+
cache_creation_tokens: usage.cache_creation_input_tokens,
|
|
299
|
+
cache_read_tokens: usage.cache_read_input_tokens
|
|
295
300
|
)
|
|
296
|
-
|
|
301
|
+
)
|
|
297
302
|
end
|
|
298
303
|
|
|
299
304
|
#--
|
|
@@ -124,33 +124,40 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
stream = @client.responses.stream(params)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
127
|
+
begin
|
|
128
|
+
stream.each do |event|
|
|
129
|
+
case event.type
|
|
130
|
+
when :"response.output_item.added"
|
|
131
|
+
handle_output_item_added_function_call(event, state: current_state, yielder: yielder) if event.item&.type == :function_call
|
|
132
|
+
when :"response.output_text.delta"
|
|
133
|
+
handle_output_text_delta(event, state: current_state, yielder: yielder)
|
|
134
|
+
when :"response.output_text.done"
|
|
135
|
+
handle_output_text_done(event, state: current_state, yielder: yielder)
|
|
136
|
+
when :"response.reasoning_summary_text.delta"
|
|
137
|
+
handle_reasoning_summary_text_delta(event, state: current_state, yielder: yielder)
|
|
138
|
+
when :"response.reasoning_summary_text.done"
|
|
139
|
+
handle_reasoning_summary_text_done(event, state: current_state, yielder: yielder)
|
|
140
|
+
when :"response.function_call_arguments.delta"
|
|
141
|
+
handle_function_call_arguments_delta(event, state: current_state, yielder: yielder)
|
|
142
|
+
when :"response.function_call_arguments.done"
|
|
143
|
+
handle_function_call_arguments_done(event, state: current_state, yielder: yielder)
|
|
144
|
+
when :"response.web_search_call.in_progress"
|
|
145
|
+
handle_web_search_status(event, status: "in_progress", yielder: yielder)
|
|
146
|
+
when :"response.web_search_call.searching"
|
|
147
|
+
handle_web_search_status(event, status: "searching", yielder: yielder)
|
|
148
|
+
when :"response.web_search_call.completed"
|
|
149
|
+
handle_web_search_status(event, status: "completed", yielder: yielder)
|
|
150
|
+
when :"response.output_item.done"
|
|
151
|
+
handle_output_item_done_web_search(event, yielder: yielder) if event.item&.type == :web_search_call
|
|
152
|
+
when :"response.completed"
|
|
153
|
+
handle_response_completed(event, state: current_state, yielder: yielder)
|
|
154
|
+
end
|
|
153
155
|
end
|
|
156
|
+
ensure
|
|
157
|
+
# OpenAI SDK does not auto-close the underlying HTTP stream when
|
|
158
|
+
# iteration is interrupted (raise / fiber cancellation), so the SSE
|
|
159
|
+
# socket leaks until GC. close is idempotent and a no-op after EOF.
|
|
160
|
+
stream.close
|
|
154
161
|
end
|
|
155
162
|
end
|
|
156
163
|
|
data/lib/riffer/version.rb
CHANGED
|
@@ -113,6 +113,19 @@ class Riffer::Agent
|
|
|
113
113
|
# : (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
114
114
|
def self.resolve_uses_tools_config: (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
115
115
|
|
|
116
|
+
# Opts this agent into tools from all MCP registrations that share any of
|
|
117
|
+
# the given tag(s).
|
|
118
|
+
#
|
|
119
|
+
# +tag+ - a String or Symbol; matched against registration manifest tags.
|
|
120
|
+
#
|
|
121
|
+
# : (String | Symbol) -> void
|
|
122
|
+
def self.use_mcp: (String | Symbol) -> void
|
|
123
|
+
|
|
124
|
+
# Returns the accumulated +use_mcp+ configurations for this agent class.
|
|
125
|
+
#
|
|
126
|
+
# : () -> Array[Hash[Symbol, untyped]]
|
|
127
|
+
def self.mcp_configs: () -> Array[Hash[Symbol, untyped]]
|
|
128
|
+
|
|
116
129
|
# Gets or sets the tool runtime for this agent.
|
|
117
130
|
#
|
|
118
131
|
# Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
|
|
@@ -352,6 +365,26 @@ class Riffer::Agent
|
|
|
352
365
|
def resolve_model: () -> String
|
|
353
366
|
|
|
354
367
|
# --
|
|
368
|
+
# : () -> Array[singleton(Riffer::Tool)]
|
|
369
|
+
def resolve_uses_tools_config: () -> Array[singleton(Riffer::Tool)]
|
|
370
|
+
|
|
371
|
+
# --
|
|
372
|
+
# : () -> Array[singleton(Riffer::Tool)]
|
|
373
|
+
def resolve_mcp_tool_classes: () -> Array[singleton(Riffer::Tool)]
|
|
374
|
+
|
|
375
|
+
# Each matching MCP registration once, with tag symbols unioned across +use_mcp+ rows.
|
|
376
|
+
#
|
|
377
|
+
# : (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
378
|
+
def gather_mcp_registrations_with_tags: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
379
|
+
|
|
380
|
+
# : (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
|
|
381
|
+
def mcp_tools_for_registration: (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
|
|
382
|
+
|
|
383
|
+
# Raises if two or more tool classes share the same +.name+ (ambiguous dispatch).
|
|
384
|
+
#
|
|
385
|
+
# : (Array[singleton(Riffer::Tool)]) -> void
|
|
386
|
+
def assert_distinct_tool_names!: (Array[singleton(Riffer::Tool)]) -> void
|
|
387
|
+
|
|
355
388
|
# : () -> Array[singleton(Riffer::Tool)]
|
|
356
389
|
def resolved_tools: () -> Array[singleton(Riffer::Tool)]
|
|
357
390
|
|
|
@@ -63,6 +63,15 @@ class Riffer::Config
|
|
|
63
63
|
| ({ ?judge_model: untyped }) -> instance
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
class Mcp < Struct[untyped]
|
|
67
|
+
attr_accessor credentials(): untyped
|
|
68
|
+
|
|
69
|
+
attr_accessor discovery_runner(): untyped
|
|
70
|
+
|
|
71
|
+
def self.new: (?credentials: untyped, ?discovery_runner: untyped) -> instance
|
|
72
|
+
| ({ ?credentials: untyped, ?discovery_runner: untyped }) -> instance
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
# Skills-related global configuration.
|
|
67
76
|
#
|
|
68
77
|
# See <tt>Riffer.config.skills.default_activate_tool</tt> and
|
|
@@ -124,6 +133,17 @@ class Riffer::Config
|
|
|
124
133
|
# Evals configuration (Struct with +judge_model+).
|
|
125
134
|
attr_reader evals: Riffer::Config::Evals
|
|
126
135
|
|
|
136
|
+
# MCP configuration (Struct with +credentials+ and +discovery_runner+).
|
|
137
|
+
#
|
|
138
|
+
# +credentials+ is an optional Proc for per-run MCP +tools/call+ HTTP headers.
|
|
139
|
+
# Signature: +->(manifest:, matched_tags:, context:) { Hash or nil }+.
|
|
140
|
+
# +nil+ from the proc at tool-resolution time omits that server's tools; +nil+
|
|
141
|
+
# at tool-call time raises Riffer::Mcp::CredentialsDeniedError.
|
|
142
|
+
#
|
|
143
|
+
# +discovery_runner+ is the Riffer::Runner used to execute tool discovery
|
|
144
|
+
# (default +Runner::Sequential+).
|
|
145
|
+
attr_reader mcp: Riffer::Config::Mcp
|
|
146
|
+
|
|
127
147
|
# Global tool runtime configuration (experimental).
|
|
128
148
|
#
|
|
129
149
|
# Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/riffer/mcp/authenticated_tool.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Wraps MCP-generated tool classes so +tools/call+ uses +Riffer.config.mcp.credentials+
|
|
4
|
+
# per invocation while delegating metadata to the inner class.
|
|
5
|
+
module Riffer::Mcp::AuthenticatedTool
|
|
6
|
+
# Returns one wrapper class per inner tool, sharing +manifest+ and +matched_tags+.
|
|
7
|
+
#
|
|
8
|
+
# --
|
|
9
|
+
# : (Array[singleton(Riffer::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Tool)]
|
|
10
|
+
def self.wrap_all: (Array[singleton(Riffer::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Tool)]
|
|
11
|
+
|
|
12
|
+
# --
|
|
13
|
+
# : (singleton(Riffer::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Tool)
|
|
14
|
+
# Class.new(Riffer::Tool) is typed as ::Class by steep — it cannot verify the subtype
|
|
15
|
+
# relationship for dynamically created anonymous classes, so the ignore is required.
|
|
16
|
+
def self.wrap_one: (singleton(Riffer::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Tool)
|
|
17
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated from lib/riffer/mcp/client.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Thin wrapper around the MCP Ruby SDK client (mcp gem v0.8+).
|
|
4
|
+
#
|
|
5
|
+
# Resolves headers (if a Proc) once at initialization, then provides
|
|
6
|
+
# +tools_list+ and +tools_call+. Used for discovery (+Manifest#discovery_headers+)
|
|
7
|
+
# and for +tools/call+ when no +credentials+ proc is configured.
|
|
8
|
+
#
|
|
9
|
+
# MCP gem API used:
|
|
10
|
+
# MCP::Client::HTTP.new(url:, headers:) — HTTP transport (requires faraday)
|
|
11
|
+
# MCP::Client.new(transport:) — client
|
|
12
|
+
# client.tools — Array<MCP::Client::Tool>
|
|
13
|
+
# client.call_tool(tool:, arguments:) — raw JSON-RPC response Hash
|
|
14
|
+
class Riffer::Mcp::Client
|
|
15
|
+
include Riffer::Helpers::Dependencies
|
|
16
|
+
|
|
17
|
+
# --
|
|
18
|
+
# : (endpoint: String, ?headers: (Hash[String, String] | Proc), ?client: untyped?) -> void
|
|
19
|
+
def initialize: (endpoint: String, ?headers: Hash[String, String] | Proc, ?client: untyped?) -> void
|
|
20
|
+
|
|
21
|
+
# Returns an array of tool definition hashes, each with +:name+, +:description+,
|
|
22
|
+
# and +:input_schema+ keys.
|
|
23
|
+
#
|
|
24
|
+
# --
|
|
25
|
+
# : () -> Array[Hash[Symbol, untyped]]
|
|
26
|
+
def tools_list: () -> Array[Hash[Symbol, untyped]]
|
|
27
|
+
|
|
28
|
+
# Calls a tool on the MCP server and returns joined text content from the response.
|
|
29
|
+
#
|
|
30
|
+
# --
|
|
31
|
+
# : (String, ?Hash[untyped, untyped]) -> String
|
|
32
|
+
def tools_call: (String, ?Hash[untyped, untyped]) -> String
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated from lib/riffer/mcp/manifest.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Riffer::Mcp::Manifest holds the configuration for a single MCP server.
|
|
4
|
+
#
|
|
5
|
+
# +name+ - String identifier used as the registration key and generated-agent identifier.
|
|
6
|
+
# +tags+ - Array[Symbol]; normalized to symbols at construction time.
|
|
7
|
+
# +endpoint+ - String HTTPS URL passed to the MCP transport.
|
|
8
|
+
# +discovery_headers+ - Hash or Proc; resolved once when building the discovery client for +tools/list+.
|
|
9
|
+
# +credentials_scope+ - Optional symbol hint: +:global+, +:tenant+, +:user+ — documents whether
|
|
10
|
+
# invocation credentials are expected to depend on tenant and/or user keys in +context+ (no ids stored).
|
|
11
|
+
# Apps may treat +:user+ as "user in tenant" and pass both keys in +context+.
|
|
12
|
+
class Riffer::Mcp::Manifest
|
|
13
|
+
attr_reader name: String
|
|
14
|
+
|
|
15
|
+
attr_reader tags: Array[Symbol]
|
|
16
|
+
|
|
17
|
+
attr_reader endpoint: String
|
|
18
|
+
|
|
19
|
+
attr_reader discovery_headers: (Hash[String, untyped] | ::Proc)?
|
|
20
|
+
|
|
21
|
+
attr_reader credentials_scope: Symbol?
|
|
22
|
+
|
|
23
|
+
# --
|
|
24
|
+
# : (name: String, endpoint: String, ?tags: Array[untyped]?, ?discovery_headers: (Hash[String, untyped] | ::Proc)?, ?credentials_scope: (String | Symbol)?) -> void
|
|
25
|
+
def initialize: (name: String, endpoint: String, ?tags: Array[untyped]?, ?discovery_headers: (Hash[String, untyped] | ::Proc)?, ?credentials_scope: (String | Symbol)?) -> void
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def valid_endpoint?: () -> untyped
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Generated from lib/riffer/mcp/registration.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Per-server state managed by Riffer::Mcp::Registry.
|
|
4
|
+
#
|
|
5
|
+
# Created when a server is registered. Discovers tools via the MCP
|
|
6
|
+
# +tools/list+ call, then generates tool classes.
|
|
7
|
+
class Riffer::Mcp::Registration
|
|
8
|
+
# The manifest that describes this server.
|
|
9
|
+
attr_reader manifest: Riffer::Mcp::Manifest
|
|
10
|
+
|
|
11
|
+
# Generated Riffer::Tool subclasses.
|
|
12
|
+
#
|
|
13
|
+
# --
|
|
14
|
+
# : () -> Array[singleton(Riffer::Tool)]
|
|
15
|
+
def tools: () -> Array[singleton(Riffer::Tool)]
|
|
16
|
+
|
|
17
|
+
# --
|
|
18
|
+
# : (Riffer::Mcp::Manifest) -> void
|
|
19
|
+
def initialize: (Riffer::Mcp::Manifest) -> void
|
|
20
|
+
|
|
21
|
+
# Retires this registration, preventing in-flight discovery from publishing
|
|
22
|
+
# state.
|
|
23
|
+
#
|
|
24
|
+
# --
|
|
25
|
+
# : () -> void
|
|
26
|
+
def retire!: () -> void
|
|
27
|
+
|
|
28
|
+
# Returns true if this registration has been retired.
|
|
29
|
+
#
|
|
30
|
+
# --
|
|
31
|
+
# : () -> bool
|
|
32
|
+
def retired?: () -> bool
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Runs tool discovery using the configured Runner.
|
|
37
|
+
#
|
|
38
|
+
# With +Runner::Sequential+ (default) discovery blocks inline. With
|
|
39
|
+
# +Runner::Threaded+ discovery runs on a pool thread but +map+ still
|
|
40
|
+
# blocks the caller — useful for Rails connection-pool isolation.
|
|
41
|
+
#
|
|
42
|
+
# --
|
|
43
|
+
# : () -> void
|
|
44
|
+
def run_discovery: () -> void
|
|
45
|
+
|
|
46
|
+
# --
|
|
47
|
+
# : () -> Riffer::Mcp::Client
|
|
48
|
+
def build_client: () -> Riffer::Mcp::Client
|
|
49
|
+
end
|