riffer 0.31.0 → 0.32.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.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/code-style.md +63 -4
  3. data/.agents/rbs-inline.md +1 -6
  4. data/.release-please-manifest.json +1 -1
  5. data/AGENTS.md +1 -2
  6. data/CHANGELOG.md +18 -0
  7. data/docs/08_MESSAGES.md +1 -1
  8. data/docs/14_MCP.md +50 -5
  9. data/docs/providers/02_AMAZON_BEDROCK.md +14 -0
  10. data/lib/riffer/agent/config.rb +42 -47
  11. data/lib/riffer/agent/context.rb +70 -50
  12. data/lib/riffer/agent/response.rb +4 -20
  13. data/lib/riffer/agent/run.rb +28 -67
  14. data/lib/riffer/agent/serializer.rb +22 -81
  15. data/lib/riffer/agent/session/repair.rb +14 -40
  16. data/lib/riffer/agent/session.rb +25 -67
  17. data/lib/riffer/agent/structured_output/result.rb +3 -11
  18. data/lib/riffer/agent/structured_output.rb +5 -13
  19. data/lib/riffer/agent.rb +74 -192
  20. data/lib/riffer/config.rb +34 -101
  21. data/lib/riffer/evals/evaluator.rb +7 -27
  22. data/lib/riffer/evals/evaluator_runner.rb +11 -19
  23. data/lib/riffer/evals/judge.rb +4 -25
  24. data/lib/riffer/evals/result.rb +1 -18
  25. data/lib/riffer/evals/run_result.rb +0 -11
  26. data/lib/riffer/evals/scenario_result.rb +0 -14
  27. data/lib/riffer/evals.rb +0 -6
  28. data/lib/riffer/guardrail.rb +4 -27
  29. data/lib/riffer/guardrails/modification.rb +0 -10
  30. data/lib/riffer/guardrails/result.rb +3 -30
  31. data/lib/riffer/guardrails/runner.rb +5 -22
  32. data/lib/riffer/guardrails/tripwire.rb +1 -19
  33. data/lib/riffer/guardrails.rb +2 -4
  34. data/lib/riffer/helpers/call_or_value.rb +4 -3
  35. data/lib/riffer/helpers/class_name_converter.rb +3 -1
  36. data/lib/riffer/helpers/dependencies.rb +5 -7
  37. data/lib/riffer/helpers.rb +0 -5
  38. data/lib/riffer/mcp/authenticated_tool.rb +9 -9
  39. data/lib/riffer/mcp/client.rb +12 -17
  40. data/lib/riffer/mcp/manifest.rb +13 -10
  41. data/lib/riffer/mcp/registration.rb +2 -11
  42. data/lib/riffer/mcp/registry.rb +44 -52
  43. data/lib/riffer/mcp/search_tool.rb +53 -0
  44. data/lib/riffer/mcp/tool_factory.rb +13 -18
  45. data/lib/riffer/mcp.rb +12 -17
  46. data/lib/riffer/messages/assistant.rb +2 -9
  47. data/lib/riffer/messages/base.rb +46 -16
  48. data/lib/riffer/messages/file_part.rb +32 -24
  49. data/lib/riffer/messages/system.rb +0 -5
  50. data/lib/riffer/messages/tool.rb +0 -10
  51. data/lib/riffer/messages/user.rb +0 -10
  52. data/lib/riffer/messages.rb +0 -7
  53. data/lib/riffer/params/boolean.rb +2 -4
  54. data/lib/riffer/params/param.rb +28 -39
  55. data/lib/riffer/params.rb +9 -21
  56. data/lib/riffer/providers/amazon_bedrock.rb +42 -28
  57. data/lib/riffer/providers/anthropic.rb +4 -9
  58. data/lib/riffer/providers/azure_open_ai.rb +3 -19
  59. data/lib/riffer/providers/base.rb +13 -26
  60. data/lib/riffer/providers/gemini.rb +4 -4
  61. data/lib/riffer/providers/mock.rb +6 -26
  62. data/lib/riffer/providers/open_ai.rb +6 -8
  63. data/lib/riffer/providers/open_router.rb +4 -10
  64. data/lib/riffer/providers/repository.rb +4 -3
  65. data/lib/riffer/providers/token_usage.rb +9 -20
  66. data/lib/riffer/providers.rb +0 -8
  67. data/lib/riffer/runner/fibers.rb +10 -16
  68. data/lib/riffer/runner/sequential.rb +1 -4
  69. data/lib/riffer/runner/threaded.rb +3 -14
  70. data/lib/riffer/runner.rb +2 -15
  71. data/lib/riffer/skills/activate_tool.rb +2 -11
  72. data/lib/riffer/skills/adapter.rb +4 -22
  73. data/lib/riffer/skills/backend.rb +7 -21
  74. data/lib/riffer/skills/config.rb +10 -31
  75. data/lib/riffer/skills/context.rb +5 -20
  76. data/lib/riffer/skills/filesystem_backend.rb +7 -25
  77. data/lib/riffer/skills/frontmatter.rb +10 -28
  78. data/lib/riffer/skills/markdown_adapter.rb +2 -9
  79. data/lib/riffer/skills/xml_adapter.rb +2 -8
  80. data/lib/riffer/stream_events/base.rb +1 -6
  81. data/lib/riffer/stream_events/guardrail_modification.rb +1 -8
  82. data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -8
  83. data/lib/riffer/stream_events/interrupt.rb +4 -7
  84. data/lib/riffer/stream_events/reasoning_delta.rb +2 -4
  85. data/lib/riffer/stream_events/reasoning_done.rb +2 -4
  86. data/lib/riffer/stream_events/skill_activation.rb +2 -4
  87. data/lib/riffer/stream_events/text_delta.rb +0 -2
  88. data/lib/riffer/stream_events/text_done.rb +1 -3
  89. data/lib/riffer/stream_events/token_usage_done.rb +1 -8
  90. data/lib/riffer/stream_events/tool_call_delta.rb +2 -3
  91. data/lib/riffer/stream_events/tool_call_done.rb +1 -3
  92. data/lib/riffer/stream_events/web_search_done.rb +1 -3
  93. data/lib/riffer/stream_events/web_search_status.rb +2 -3
  94. data/lib/riffer/stream_events.rb +0 -10
  95. data/lib/riffer/tool.rb +6 -13
  96. data/lib/riffer/tools/response.rb +8 -4
  97. data/lib/riffer/tools/runtime/fibers.rb +0 -3
  98. data/lib/riffer/tools/runtime/inline.rb +1 -4
  99. data/lib/riffer/tools/runtime/threaded.rb +0 -2
  100. data/lib/riffer/tools/runtime.rb +5 -38
  101. data/lib/riffer/tools/toolable.rb +5 -16
  102. data/lib/riffer/tools.rb +0 -4
  103. data/lib/riffer/version.rb +1 -1
  104. data/lib/riffer.rb +7 -8
  105. data/sig/generated/riffer/agent/config.rbs +29 -46
  106. data/sig/generated/riffer/agent/context.rbs +40 -48
  107. data/sig/generated/riffer/agent/response.rbs +4 -20
  108. data/sig/generated/riffer/agent/run.rbs +12 -61
  109. data/sig/generated/riffer/agent/serializer.rbs +21 -80
  110. data/sig/generated/riffer/agent/session/repair.rbs +12 -40
  111. data/sig/generated/riffer/agent/session.rbs +25 -67
  112. data/sig/generated/riffer/agent/structured_output/result.rbs +2 -10
  113. data/sig/generated/riffer/agent/structured_output.rbs +5 -12
  114. data/sig/generated/riffer/agent.rbs +57 -186
  115. data/sig/generated/riffer/config.rbs +34 -100
  116. data/sig/generated/riffer/evals/evaluator.rbs +7 -27
  117. data/sig/generated/riffer/evals/evaluator_runner.rbs +9 -19
  118. data/sig/generated/riffer/evals/judge.rbs +4 -24
  119. data/sig/generated/riffer/evals/result.rbs +1 -17
  120. data/sig/generated/riffer/evals/run_result.rbs +0 -10
  121. data/sig/generated/riffer/evals/scenario_result.rbs +0 -13
  122. data/sig/generated/riffer/evals.rbs +0 -6
  123. data/sig/generated/riffer/guardrail.rbs +4 -27
  124. data/sig/generated/riffer/guardrails/modification.rbs +0 -10
  125. data/sig/generated/riffer/guardrails/result.rbs +3 -30
  126. data/sig/generated/riffer/guardrails/runner.rbs +5 -22
  127. data/sig/generated/riffer/guardrails/tripwire.rbs +1 -19
  128. data/sig/generated/riffer/guardrails.rbs +2 -4
  129. data/sig/generated/riffer/helpers/call_or_value.rbs +4 -3
  130. data/sig/generated/riffer/helpers/class_name_converter.rbs +1 -1
  131. data/sig/generated/riffer/helpers/dependencies.rbs +3 -7
  132. data/sig/generated/riffer/helpers.rbs +0 -5
  133. data/sig/generated/riffer/mcp/authenticated_tool.rbs +5 -4
  134. data/sig/generated/riffer/mcp/client.rbs +10 -16
  135. data/sig/generated/riffer/mcp/manifest.rbs +9 -9
  136. data/sig/generated/riffer/mcp/registration.rbs +2 -10
  137. data/sig/generated/riffer/mcp/registry.rbs +11 -18
  138. data/sig/generated/riffer/mcp/search_tool.rbs +26 -0
  139. data/sig/generated/riffer/mcp/tool_factory.rbs +10 -15
  140. data/sig/generated/riffer/mcp.rbs +10 -17
  141. data/sig/generated/riffer/messages/assistant.rbs +2 -8
  142. data/sig/generated/riffer/messages/base.rbs +11 -16
  143. data/sig/generated/riffer/messages/file_part.rbs +13 -23
  144. data/sig/generated/riffer/messages/system.rbs +0 -4
  145. data/sig/generated/riffer/messages/tool.rbs +0 -9
  146. data/sig/generated/riffer/messages/user.rbs +0 -9
  147. data/sig/generated/riffer/messages.rbs +0 -7
  148. data/sig/generated/riffer/params/boolean.rbs +2 -4
  149. data/sig/generated/riffer/params/param.rbs +21 -39
  150. data/sig/generated/riffer/params.rbs +9 -21
  151. data/sig/generated/riffer/providers/amazon_bedrock.rbs +21 -25
  152. data/sig/generated/riffer/providers/anthropic.rbs +2 -7
  153. data/sig/generated/riffer/providers/azure_open_ai.rbs +3 -18
  154. data/sig/generated/riffer/providers/base.rbs +9 -25
  155. data/sig/generated/riffer/providers/gemini.rbs +0 -2
  156. data/sig/generated/riffer/providers/mock.rbs +6 -26
  157. data/sig/generated/riffer/providers/open_ai.rbs +1 -5
  158. data/sig/generated/riffer/providers/open_router.rbs +4 -10
  159. data/sig/generated/riffer/providers/repository.rbs +2 -3
  160. data/sig/generated/riffer/providers/token_usage.rbs +6 -16
  161. data/sig/generated/riffer/providers.rbs +0 -8
  162. data/sig/generated/riffer/runner/fibers.rbs +8 -15
  163. data/sig/generated/riffer/runner/sequential.rbs +1 -3
  164. data/sig/generated/riffer/runner/threaded.rbs +3 -13
  165. data/sig/generated/riffer/runner.rbs +2 -14
  166. data/sig/generated/riffer/skills/activate_tool.rbs +2 -11
  167. data/sig/generated/riffer/skills/adapter.rbs +4 -22
  168. data/sig/generated/riffer/skills/backend.rbs +7 -21
  169. data/sig/generated/riffer/skills/config.rbs +10 -31
  170. data/sig/generated/riffer/skills/context.rbs +5 -20
  171. data/sig/generated/riffer/skills/filesystem_backend.rbs +7 -24
  172. data/sig/generated/riffer/skills/frontmatter.rbs +10 -27
  173. data/sig/generated/riffer/skills/markdown_adapter.rbs +2 -9
  174. data/sig/generated/riffer/skills/xml_adapter.rbs +2 -8
  175. data/sig/generated/riffer/stream_events/base.rbs +1 -6
  176. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +1 -8
  177. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +1 -8
  178. data/sig/generated/riffer/stream_events/interrupt.rbs +4 -7
  179. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +2 -4
  180. data/sig/generated/riffer/stream_events/reasoning_done.rbs +2 -4
  181. data/sig/generated/riffer/stream_events/skill_activation.rbs +2 -4
  182. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  183. data/sig/generated/riffer/stream_events/text_done.rbs +1 -3
  184. data/sig/generated/riffer/stream_events/token_usage_done.rbs +1 -7
  185. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +2 -3
  186. data/sig/generated/riffer/stream_events/tool_call_done.rbs +1 -3
  187. data/sig/generated/riffer/stream_events/web_search_done.rbs +1 -3
  188. data/sig/generated/riffer/stream_events/web_search_status.rbs +2 -3
  189. data/sig/generated/riffer/stream_events.rbs +0 -10
  190. data/sig/generated/riffer/tool.rbs +5 -12
  191. data/sig/generated/riffer/tools/response.rbs +6 -4
  192. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -3
  193. data/sig/generated/riffer/tools/runtime/inline.rbs +1 -3
  194. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -2
  195. data/sig/generated/riffer/tools/runtime.rbs +5 -37
  196. data/sig/generated/riffer/tools/toolable.rbs +4 -14
  197. data/sig/generated/riffer/tools.rbs +0 -4
  198. data/sig/generated/riffer.rbs +5 -4
  199. data/sig/manual/riffer/agent/session/repair.rbs +5 -0
  200. data/sig/manual/riffer/evals/evaluator_runner.rbs +5 -0
  201. data/sig/manual/riffer/helpers/class_name_converter.rbs +5 -0
  202. data/sig/manual/riffer/helpers/dependencies.rbs +5 -0
  203. data/sig/manual/riffer/mcp/authenticated_tool.rbs +5 -0
  204. data/sig/manual/riffer/mcp/registry.rbs +5 -0
  205. data/sig/manual/riffer/mcp/tool_factory.rbs +5 -0
  206. data/sig/manual/riffer/mcp.rbs +5 -0
  207. data/sig/manual/riffer/providers/repository.rbs +5 -0
  208. data/sig/manual/riffer.rbs +5 -0
  209. metadata +17 -9
  210. data/.agents/rdoc.md +0 -69
  211. data/lib/riffer/messages/converter.rb +0 -90
  212. data/sig/generated/riffer/messages/converter.rbs +0 -33
  213. data/sig/manual/riffer/tools/toolable.rbs +0 -6
@@ -1,14 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Executes guardrails sequentially and manages the processing pipeline.
5
- #
6
- # The runner processes guardrails in order, passing the output of each
7
- # to the next. If any guardrail blocks, execution stops and a tripwire
8
- # is returned.
9
- #
10
- # runner = Runner.new(guardrail_configs, phase: :before, context: context)
11
- # data, tripwire, modifications = runner.run(messages)
4
+ # Executes guardrails sequentially, passing each one's output to the next; if
5
+ # any blocks, execution stops and a tripwire is returned.
12
6
  class Riffer::Guardrails::Runner
13
7
  # The guardrail configs to execute.
14
8
  attr_reader :guardrail_configs #: Array[Hash[Symbol, untyped]]
@@ -19,12 +13,6 @@ class Riffer::Guardrails::Runner
19
13
  # The context passed to guardrails.
20
14
  attr_reader :context #: untyped
21
15
 
22
- # Creates a new runner.
23
- #
24
- # [guardrail_configs] configs with :class and :options keys.
25
- # [phase] :before or :after.
26
- # [context] optional context to pass to guardrails.
27
- #
28
16
  #--
29
17
  #: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped) -> void
30
18
  def initialize(guardrail_configs, phase:, context: nil)
@@ -33,14 +21,9 @@ class Riffer::Guardrails::Runner
33
21
  @context = context
34
22
  end
35
23
 
36
- # Runs the guardrails sequentially.
37
- #
38
- # For before phase, data should be an array of messages.
39
- # For after phase, data should be a response and messages must be provided.
40
- #
41
- # [data] the data to process (messages for before, response for after).
42
- # [messages] the conversation messages (required for after phase).
43
- #
24
+ # Runs the guardrails sequentially. For the +:before+ phase +data+ is the
25
+ # messages array; for +:after+ it's the response (and +messages+ must be
26
+ # provided).
44
27
  #--
45
28
  #: (untyped, ?messages: Array[Riffer::Messages::Base]?) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
46
29
  def run(data, messages: nil)
@@ -2,16 +2,6 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  # Captures information about a blocked guardrail execution.
5
- #
6
- # When a guardrail blocks execution, a Tripwire is created to record
7
- # the reason, which guardrail triggered it, and which phase it occurred in.
8
- #
9
- # tripwire = Tripwire.new(
10
- # reason: "PII detected in input",
11
- # guardrail: PiiRedactor,
12
- # phase: :before,
13
- # metadata: { detected_types: [:email, :phone] }
14
- # )
15
5
  class Riffer::Guardrails::Tripwire
16
6
  PHASES = Riffer::Guardrails::PHASES #: Array[Symbol]
17
7
 
@@ -27,15 +17,7 @@ class Riffer::Guardrails::Tripwire
27
17
  # Optional metadata about the block.
28
18
  attr_reader :metadata #: Hash[Symbol, untyped]?
29
19
 
30
- # Creates a new tripwire.
31
- #
32
- # [reason] the reason for blocking.
33
- # [guardrail] the guardrail class that blocked.
34
- # [phase] :before or :after.
35
- # [metadata] optional additional information.
36
- #
37
- # Raises Riffer::ArgumentError if the phase is invalid.
38
- #
20
+ # Raises Riffer::ArgumentError if +phase+ is invalid.
39
21
  #--
40
22
  #: (reason: String, guardrail: singleton(Riffer::Guardrail), phase: Symbol, ?metadata: Hash[Symbol, untyped]?) -> void
41
23
  def initialize(reason:, guardrail:, phase:, metadata: nil)
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Namespace module for guardrail components.
5
- #
6
- # Guardrails provide pre-processing of input messages and post-processing
7
- # of output responses in the agent pipeline.
4
+ # Namespace for guardrail components that pre-process input and post-process
5
+ # output in the agent pipeline.
8
6
  module Riffer::Guardrails
9
7
  PHASES = %i[before after].freeze #: Array[Symbol]
10
8
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Resolves the "Proc-or-value" idiom: if +thing+ is a Proc, calls it
5
- # (passing +context+ when its arity is non-zero); otherwise returns
6
- # +thing+ unchanged. When +thing+ is +nil+, returns +default+.
4
+ # Resolves the Proc-or-value idiom.
7
5
  module Riffer::Helpers::CallOrValue
8
6
  extend self
9
7
 
8
+ # Calls +thing+ when it's a Proc (passing +context+ if its arity is non-zero),
9
+ # returns it unchanged otherwise, or +default+ when +nil+.
10
+ #--
10
11
  #: (untyped, ?context: untyped, ?default: untyped) -> untyped
11
12
  def resolve(thing, context: nil, default: nil)
12
13
  return default if thing.nil?
@@ -3,13 +3,15 @@
3
3
 
4
4
  # Helper module for converting class names.
5
5
  module Riffer::Helpers::ClassNameConverter
6
+ extend self
7
+
6
8
  DEFAULT_SEPARATOR = "/" #: String
7
9
 
8
10
  # Converts a class name to snake_case identifier format.
9
11
  #
10
12
  #--
11
13
  #: (String, ?separator: String) -> String
12
- def class_name_to_path(class_name, separator: DEFAULT_SEPARATOR)
14
+ def convert(class_name, separator: DEFAULT_SEPARATOR)
13
15
  class_name
14
16
  .to_s
15
17
  .gsub("::", separator)
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Helper module for lazy loading gem dependencies.
5
- #
6
- # Used by providers to load their required gems only when needed.
4
+ # Lazy-loads gem dependencies used by providers to load required gems only
5
+ # when needed.
7
6
  module Riffer::Helpers::Dependencies
7
+ extend self
8
+
8
9
  # Raised when a required gem cannot be loaded.
9
10
  class LoadError < ::LoadError; end
10
11
 
11
- # Requires a gem by name, raising a helpful error if it is not installed.
12
- #
13
- # Raises LoadError if the gem cannot be required.
14
- #
12
+ # Requires a gem by name; raises LoadError if it isn't installed.
15
13
  #--
16
14
  #: (String) -> true
17
15
  def depends_on(gem_name)
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Namespace for shared helper modules in the Riffer framework.
5
- #
6
- # Helpers provide reusable functionality across the library:
7
- # - Riffer::Helpers::ClassNameConverter - Class name to path conversion
8
- # - Riffer::Helpers::Dependencies - Lazy gem dependency loading
9
4
  module Riffer::Helpers
10
5
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Wraps MCP-generated tool classes so +tools/call+ uses +Riffer.config.mcp.credentials+
5
- # per invocation while delegating metadata to the inner class.
6
- #
4
+ # Wraps MCP-generated tool classes so +tools/call+ resolves
5
+ # +Riffer.config.mcp.credentials+ per invocation, delegating metadata to the
6
+ # inner class.
7
7
  module Riffer::Mcp::AuthenticatedTool
8
+ extend self
9
+
8
10
  # Returns one wrapper class per inner tool, sharing +manifest+ and +matched_tags+.
9
11
  #
10
12
  #--
11
13
  #: (Array[singleton(Riffer::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Tool)]
12
- def self.wrap_all(tool_classes, manifest, matched_tags)
14
+ def wrap_all(tool_classes, manifest, matched_tags)
13
15
  tool_classes.map { |tc| wrap_one(tc, manifest, matched_tags) }
14
16
  end
15
17
 
@@ -17,7 +19,7 @@ module Riffer::Mcp::AuthenticatedTool
17
19
  #: (singleton(Riffer::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Tool)
18
20
  # Class.new(Riffer::Tool) is typed as ::Class by steep — it cannot verify the subtype
19
21
  # relationship for dynamically created anonymous classes, so the ignore is required.
20
- def self.wrap_one(inner_class, manifest, matched_tags) # steep:ignore MethodBodyTypeMismatch
22
+ def wrap_one(inner_class, manifest, matched_tags) # steep:ignore MethodBodyTypeMismatch
21
23
  inner = inner_class
22
24
  man = manifest
23
25
  tags = matched_tags
@@ -33,10 +35,8 @@ module Riffer::Mcp::AuthenticatedTool
33
35
  define_singleton_method(:description) { inner.description }
34
36
  define_singleton_method(:parameters_schema) { |strict: false| inner.parameters_schema(strict: strict) }
35
37
 
36
- # Builds a client for a single +tools/call+ invocation.
37
- #
38
- # Creates a fresh client per call so headers from the credentials proc stay
39
- # current.
38
+ # Creates a fresh client per +tools/call+ so headers from the credentials
39
+ # proc stay current.
40
40
  # TODO: A per-headers cache would reduce connection churn under load, and
41
41
  # requires a follow-up investigation to determine how to invalidate failing
42
42
  # clients.
@@ -1,21 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Thin wrapper around the MCP Ruby SDK client (mcp gem v0.8+).
5
- #
6
- # Resolves headers (if a Proc) once at initialization, then provides
7
- # +tools_list+ and +tools_call+. Used for discovery (+Manifest#discovery_headers+)
8
- # and for +tools/call+ when no +credentials+ proc is configured.
9
- #
10
- # MCP gem API used:
11
- # MCP::Client::HTTP.new(url:, headers:) — HTTP transport (requires faraday)
12
- # MCP::Client.new(transport:) — client
13
- # client.tools — Array<MCP::Client::Tool>
14
- # client.call_tool(tool:, arguments:) — raw JSON-RPC response Hash
15
- #
4
+ # Thin wrapper around the MCP Ruby SDK client (mcp gem v0.8+). Resolves headers
5
+ # (if a Proc) once at init, then provides +tools_list+ / +tools_call+ — used for
6
+ # discovery and for +tools/call+ when no +credentials+ proc is configured.
16
7
  class Riffer::Mcp::Client
17
- include Riffer::Helpers::Dependencies
18
-
19
8
  # @rbs @client: untyped
20
9
 
21
10
  #--
@@ -31,9 +20,8 @@ class Riffer::Mcp::Client
31
20
  end
32
21
  end
33
22
 
34
- # Returns an array of tool definition hashes, each with +:name+, +:description+,
35
- # and +:input_schema+ keys.
36
- #
23
+ # Returns tool definition hashes with +:name+, +:description+, and
24
+ # +:input_schema+ keys.
37
25
  #--
38
26
  #: () -> Array[Hash[Symbol, untyped]]
39
27
  def tools_list
@@ -66,4 +54,11 @@ class Riffer::Mcp::Client
66
54
  content = response.dig("result", "content") || []
67
55
  content.filter_map { |item| item["text"] }.join
68
56
  end
57
+
58
+ private
59
+
60
+ #: (String) -> true
61
+ def depends_on(gem_name)
62
+ Riffer::Helpers::Dependencies.depends_on(gem_name)
63
+ end
69
64
  end
@@ -3,23 +3,26 @@
3
3
 
4
4
  require "uri"
5
5
 
6
- # Riffer::Mcp::Manifest holds the configuration for a single MCP server.
7
- #
8
- # +name+ - String identifier used as the registration key and generated-agent identifier.
9
- # +tags+ - Array[Symbol]; normalized to symbols at construction time.
10
- # +endpoint+ - String HTTPS URL passed to the MCP transport.
11
- # +discovery_headers+ - Hash or Proc; resolved once when building the discovery client for +tools/list+.
12
- # +credentials_scope+ - Optional symbol hint: +:global+, +:tenant+, +:user+ — documents whether
13
- # invocation credentials are expected to depend on tenant and/or user keys in +context+ (no ids stored).
14
- # Apps may treat +:user+ as "user in tenant" and pass both keys in +context+.
15
- #
6
+ # Holds the configuration for a single MCP server.
16
7
  class Riffer::Mcp::Manifest
8
+ # Identifier used as the registration key and generated-agent identifier.
17
9
  attr_reader :name #: String
10
+
11
+ # Tags for matching +use_mcp+.
18
12
  attr_reader :tags #: Array[Symbol]
13
+
14
+ # HTTPS URL passed to the MCP transport.
19
15
  attr_reader :endpoint #: String
16
+
17
+ # Headers (or a Proc) resolved once when building the discovery client.
20
18
  attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)?
19
+
20
+ # Optional hint (+:global+/+:tenant+/+:user+) for whether invocation
21
+ # credentials depend on tenant/user keys in +context+.
21
22
  attr_reader :credentials_scope #: Symbol?
22
23
 
24
+ # Raises Riffer::ArgumentError unless +name+ is present and +endpoint+ is a
25
+ # valid HTTPS URL.
23
26
  #--
24
27
  #: (name: String, endpoint: String, ?tags: Array[untyped]?, ?discovery_headers: (Hash[String, untyped] | ::Proc)?, ?credentials_scope: (String | Symbol)?) -> void
25
28
  def initialize(name:, endpoint:, tags: nil, discovery_headers: nil, credentials_scope: nil)
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
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
- #
4
+ # Per-server state managed by Riffer::Mcp::Registry — discovers tools via
5
+ # +tools/list+ and generates tool classes when a server is registered.
9
6
  class Riffer::Mcp::Registration
10
7
  # @rbs @cancelled: bool
11
8
  # @rbs @tools: Array[singleton(Riffer::Tool)]
@@ -51,12 +48,6 @@ class Riffer::Mcp::Registration
51
48
 
52
49
  private
53
50
 
54
- # Runs tool discovery using the configured Runner.
55
- #
56
- # With +Runner::Sequential+ (default) discovery blocks inline. With
57
- # +Runner::Threaded+ discovery runs on a pool thread but +map+ still
58
- # blocks the caller — useful for Rails connection-pool isolation.
59
- #
60
51
  #--
61
52
  #: () -> void
62
53
  def run_discovery
@@ -1,67 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Thread-safe global store for MCP server registrations.
5
- #
6
- # Keyed by manifest name. All public methods are mutex-guarded.
7
- #
4
+ # Thread-safe global store for MCP server registrations, keyed by manifest name.
8
5
  module Riffer::Mcp::Registry
9
- # @rbs self.@mutex: Thread::Mutex
10
- # @rbs self.@store: Hash[String, Riffer::Mcp::Registration]
6
+ extend self
7
+
8
+ # @rbs @mutex: Thread::Mutex
9
+ # @rbs @store: Hash[String, Riffer::Mcp::Registration]
11
10
 
12
11
  @mutex = Mutex.new
13
12
  @store = {} #: Hash[String, Riffer::Mcp::Registration]
14
13
 
15
- class << self
16
- # Registers an MCP server and starts async tool discovery.
17
- #
18
- # Accepts a Manifest instance or a hash of manifest keyword arguments.
19
- # Replaces any existing registration with the same name.
20
- #
21
- #--
22
- #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
23
- def register(manifest_or_hash)
24
- # steep cannot verify that an untyped Hash splat supplies Manifest's
25
- # required name:/endpoint: keywords; Manifest validates them at runtime.
26
- manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
27
- registration = Riffer::Mcp::Registration.new(manifest)
28
- old = @mutex.synchronize do
29
- previous = @store[manifest.name]
30
- @store[manifest.name] = registration
31
- previous
32
- end
33
- old&.retire!
34
- registration
14
+ # Registers an MCP server and starts async tool discovery, replacing any
15
+ # existing registration with the same name.
16
+ #--
17
+ #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
18
+ def register(manifest_or_hash)
19
+ # steep cannot verify that an untyped Hash splat supplies Manifest's
20
+ # required name:/endpoint: keywords; Manifest validates them at runtime.
21
+ manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
22
+ registration = Riffer::Mcp::Registration.new(manifest)
23
+ old = @mutex.synchronize do
24
+ previous = @store[manifest.name]
25
+ @store[manifest.name] = registration
26
+ previous
35
27
  end
28
+ old&.retire!
29
+ registration
30
+ end
36
31
 
37
- # Removes a registration by name.
38
- #
39
- #--
40
- #: ((String | Symbol)) -> void
41
- def unregister(name)
42
- removed = @mutex.synchronize { @store.delete(name.to_s) }
43
- removed&.retire!
44
- end
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
45
40
 
46
- # Returns a frozen snapshot of all current registrations.
47
- #
48
- #--
49
- #: () -> Hash[String, Riffer::Mcp::Registration]
50
- def registrations
51
- @mutex.synchronize { @store.dup.freeze }
52
- end
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
53
48
 
54
- # Returns all registrations whose manifest tags intersect with the given tags.
55
- #
56
- # Tags are normalized to symbols before matching.
57
- #
58
- #--
59
- #: (Array[Symbol]) -> Array[Riffer::Mcp::Registration]
60
- def find_by_tags(tags)
61
- normalized = tags.map(&:to_sym)
62
- @mutex.synchronize do
63
- @store.values.select { |reg| (reg.manifest.tags & normalized).any? }
64
- end
49
+ # Returns all registrations whose manifest tags intersect the given tags
50
+ # (normalized to symbols).
51
+ #--
52
+ #: (Array[Symbol]) -> Array[Riffer::Mcp::Registration]
53
+ def find_by_tags(tags)
54
+ normalized = tags.map(&:to_sym)
55
+ @mutex.synchronize do
56
+ @store.values.select { |reg| (reg.manifest.tags & normalized).any? }
65
57
  end
66
58
  end
67
59
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Searches available MCP tools by name or description.
5
+ class Riffer::Mcp::SearchTool < Riffer::Tool
6
+ IDENTIFIER = "mcp_search"
7
+
8
+ # Successful search response carrying the matched tool classes.
9
+ class Result < Riffer::Tools::Response
10
+ # Tool classes that matched the search query.
11
+ attr_reader :discovered_tools #: Array[singleton(Riffer::Tool)]
12
+
13
+ #--
14
+ #: (String, Array[singleton(Riffer::Tool)]) -> void
15
+ def initialize(content, discovered_tools)
16
+ super(content: content, success: true)
17
+ @discovered_tools = discovered_tools
18
+ end
19
+ end
20
+
21
+ identifier IDENTIFIER
22
+ description "Search for available MCP tools by name or description."
23
+
24
+ params do
25
+ required :query, String, description: "Non-empty substring to filter tools by name or description."
26
+ end
27
+
28
+ # Searches progressive MCP tools and returns a +Result+ with +discovered_tools+.
29
+ #--
30
+ #: (context: Riffer::Agent::Context?, query: String) -> Riffer::Tools::Response
31
+ def call(context:, query:)
32
+ return error("Provide a search query to find MCP tools by name or description.") if query.strip.empty?
33
+
34
+ tools = context&.mcp_progressive_tools || []
35
+ matches = filter(tools, query)
36
+
37
+ return text("No tools found matching '#{query}'.") if matches.empty?
38
+
39
+ names = matches.map(&:name).join(", ")
40
+ Result.new(
41
+ "Found #{matches.length} tool(s): #{names}. They are now available — call them directly.",
42
+ matches
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ #: (Array[singleton(Riffer::Tool)], String) -> Array[singleton(Riffer::Tool)]
49
+ def filter(tools, query)
50
+ q = query.downcase
51
+ tools.select { |t| t.name.downcase.include?(q) || t.description.to_s.downcase.include?(q) }
52
+ end
53
+ end
@@ -2,33 +2,28 @@
2
2
  # rbs_inline: enabled
3
3
 
4
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
- #
5
+ # Generated tools delegate +#call+ to the MCP client and skip Riffer's param
6
+ # validation the MCP server validates inputs.
11
7
  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
- #
8
+ extend self
9
+
10
+ # Builds one Riffer::Tool subclass per tool definition, prefixing names with
11
+ # the manifest name to avoid cross-server collisions (e.g. +jira__search+);
12
+ # the server-side name stays on +.mcp_server_tool_name+.
18
13
  #--
19
14
  #: (String, Riffer::Mcp::Client, Array[Hash[Symbol, untyped]]) -> Array[singleton(Riffer::Tool)]
20
- def self.build(manifest_name, client, tool_defs)
15
+ def build(manifest_name, client, tool_defs)
21
16
  tool_defs.map { |td| build_tool_class(manifest_name, client, td) }
22
17
  end
23
18
 
24
- # Replaces characters that are unsafe in LLM tool names.
19
+ private
20
+
25
21
  #: (String) -> String
26
- def self.sanitize_name_component(str)
22
+ def sanitize_name_component(str)
27
23
  str.gsub(/[^a-zA-Z0-9_-]/, "_")
28
24
  end
29
- private_class_method :sanitize_name_component
30
25
 
31
- private_class_method def self.build_tool_class(manifest_name, client, td)
26
+ def build_tool_class(manifest_name, client, td)
32
27
  prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
33
28
 
34
29
  # steep cannot type the body of a dynamically created anonymous class:
@@ -39,7 +34,7 @@ module Riffer::Mcp::ToolFactory
39
34
  @mcp_client = client
40
35
  @mcp_server_tool_name = td[:name]
41
36
  # Set @identifier directly so .identifier does not fall back to
42
- # class_name_to_path(nil) on this anonymous class.
37
+ # Riffer::Helpers::ClassNameConverter.convert(nil) on this anonymous class.
43
38
  @identifier = prefixed
44
39
 
45
40
  define_singleton_method(:name) { prefixed }
data/lib/riffer/mcp.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
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).
4
+ # Integration with Model Context Protocol (MCP) servers. Register servers
5
+ # globally; agents opt-in by tag via the +use_mcp+ DSL. Tags are
6
+ # application-defined; see +docs/14_MCP.md+.
8
7
  #
9
8
  # Riffer::Mcp.register(
10
9
  # name: "github",
@@ -19,6 +18,8 @@
19
18
  # end
20
19
  #
21
20
  module Riffer::Mcp
21
+ extend self
22
+
22
23
  # Base error for all MCP-related failures.
23
24
  class Error < Riffer::Error; end
24
25
 
@@ -26,32 +27,26 @@ module Riffer::Mcp
26
27
  # after the server's tools were included for this run.
27
28
  class CredentialsDeniedError < Error; end
28
29
 
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
- #
30
+ # Registers an MCP server, blocking until tool discovery completes. Raises
31
+ # on discovery failure.
34
32
  #--
35
33
  #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
36
- def self.register(manifest_or_hash)
34
+ def register(manifest_or_hash)
37
35
  Registry.register(manifest_or_hash)
38
36
  end
39
37
 
40
- # Removes a registration by name.
41
- #
42
- # Subsequent agent runs will not see tools from this server.
43
- #
38
+ # Removes a registration by name; subsequent agent runs won't see its tools.
44
39
  #--
45
40
  #: (String) -> void
46
- def self.unregister(name)
41
+ def unregister(name)
47
42
  Registry.unregister(name)
48
43
  end
49
44
 
50
- # Returns all current registrations keyed by name (for introspection).
45
+ # Returns all current registrations keyed by name.
51
46
  #
52
47
  #--
53
48
  #: () -> Hash[String, Riffer::Mcp::Registration]
54
- def self.registrations
49
+ def registrations
55
50
  Registry.registrations
56
51
  end
57
52
  end
@@ -1,15 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Represents an assistant (LLM) message in a conversation.
5
- #
6
- # May include tool calls when the LLM requests tool execution.
7
- #
8
- # msg = Riffer::Messages::Assistant.new("Hello!")
9
- # msg.role # => :assistant
10
- # msg.content # => "Hello!"
11
- # msg.tool_calls # => []
12
- #
4
+ # Represents an assistant (LLM) message in a conversation; may include tool
5
+ # calls when the LLM requests tool execution.
13
6
  class Riffer::Messages::Assistant < Riffer::Messages::Base
14
7
  ToolCall = Struct.new(:call_id, :name, :arguments, keyword_init: true)
15
8