Class: Nexo::MCP::GatedTool
- Inherits:
-
Object
- Object
- Nexo::MCP::GatedTool
- Defined in:
- lib/nexo/mcp/gated_tool.rb
Overview
Wraps a ruby_llm-mcp tool so the chat sees an identical tool but every
invocation is authorized through Nexo::Permissions#authorize_mcp! first. A
denied call returns { error: ... } (recoverable) and never raises into the
loop — matching the sandbox-backed tools' authorize→act→rescue-+Denied+→
{error:} shape (see lib/nexo/tools/write_file.rb).
Everything except the execution method is delegated to the wrapped tool, so the chat cannot tell the wrapper apart from the raw MCP tool: it reports the same +name+/+description+/+params_schema+ and serializes identically.
VERIFY (Group 0, ruby_llm-mcp 1.0.0 + ruby_llm 1.16.0): RubyLLM::Chat#with_tool
does not type-check — it accepts any object responding to #name — and the tool
loop invokes tool.call(args) with a positional Hash. So a duck-typed wrapper
is accepted; no RubyLLM::Tool subclass is required. #call here mirrors that
entry point (a +RubyLLM::MCP::Tool+'s own #call normalizes and dispatches to
#execute), and method_missing forwards +description+/+params_schema+/+to_h+/
etc. to the wrapped tool.
Instance Method Summary collapse
-
#call(args = {}) ⇒ Object
Authorizes the call by tool name, then delegates to the wrapped tool.
-
#execute(*args, **kwargs) ⇒ Object
The gate lives on #call: #execute (the UNGATED tool body) must never be reachable through the wrapper, or a caller invoking #execute directly would skip authorization entirely.
-
#initialize(tool:, permissions:) ⇒ GatedTool
constructor
Wraps
tool(aruby_llm-mcptool) so every call is authorized throughpermissions(the MCP axis) before delegating. -
#method_missing(method_name, *args, **kwargs, &block) ⇒ Object
Delegates any unknown method to the wrapped tool (description, params_schema, to_h, ...), so the wrapper serializes identically.
-
#name ⇒ Object
The wrapped tool's name — what the chat and the gate key on.
-
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
Forward description/params_schema/to_h/etc.
Constructor Details
#initialize(tool:, permissions:) ⇒ GatedTool
Wraps tool (a ruby_llm-mcp tool) so every call is authorized through
permissions (the MCP axis) before delegating.
25 26 27 28 |
# File 'lib/nexo/mcp/gated_tool.rb', line 25 def initialize(tool:, permissions:) @tool = tool @permissions = end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_name, *args, **kwargs, &block) ⇒ Object
Delegates any unknown method to the wrapped tool (description,
params_schema, to_h, ...), so the wrapper serializes identically. execute
is handled explicitly above and never falls through here.
61 62 63 64 65 66 67 |
# File 'lib/nexo/mcp/gated_tool.rb', line 61 def method_missing(method_name, *args, **kwargs, &block) if @tool.respond_to?(method_name) @tool.public_send(method_name, *args, **kwargs, &block) else super end end |
Instance Method Details
#call(args = {}) ⇒ Object
Authorizes the call by tool name, then delegates to the wrapped tool. A
denial is returned as { error: ... } so the model can recover — never
raised into the loop.
33 34 35 36 37 38 |
# File 'lib/nexo/mcp/gated_tool.rb', line 33 def call(args = {}) @permissions.(name, args) @tool.call(args) rescue Nexo::Permissions::Denied => e {error: e.} end |
#execute(*args, **kwargs) ⇒ Object
The gate lives on #call: #execute (the UNGATED tool body) must never be reachable through the wrapper, or a caller invoking #execute directly would skip authorization entirely. Route it back to the gated #call.
46 47 48 |
# File 'lib/nexo/mcp/gated_tool.rb', line 46 def execute(*args, **kwargs) call(kwargs.empty? ? (args.first || {}) : kwargs) end |
#name ⇒ Object
The wrapped tool's name — what the chat and the gate key on.
41 |
# File 'lib/nexo/mcp/gated_tool.rb', line 41 def name = @tool.name |
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
Forward description/params_schema/to_h/etc. to the wrapped tool so the chat
serializes it exactly like the raw MCP tool — but never execute (see above).
52 53 54 55 56 |
# File 'lib/nexo/mcp/gated_tool.rb', line 52 def respond_to_missing?(method_name, include_private = false) return true if method_name == :execute @tool.respond_to?(method_name, include_private) || super end |