Class: LittleGhost::MCP::Toolset

Inherits:
Object
  • Object
show all
Extended by:
Support::ClassAttributes
Defined in:
lib/little_ghost/mcp/toolset.rb

Overview

Loads remote Tool classes through an application-created official MCP::Client. The client factory runs during Tool discovery with the current Tool::Binding. It must return a fresh, unconnected client without starting remote work. After the factory returns, LittleGhost connects the client, shares it among the generated Tool instances, and calls close on its transport, when supported, as the owning run or ToolRegistry closes.

Discovery and Tool calls carry the run's cancellation and deadline into the SDK. The official SDK executes cancellable requests on worker threads. Calls through the official HTTP transport may overlap. When the official stdio transport is supplied directly, calls are serialized and cancelling one invalidates that run's session. Custom or decorated transports own their serialization, cancellation-safe invalidation, and cleanup. SDK handlers may run on worker or listener threads, so application callbacks must support concurrent use and must not depend on the calling fiber's local state. Treat handler requests as untrusted and authorize any local work or data they can reach.

class HelpCenterTools < LittleGhost::MCP::Toolset
client do |_binding|
  transport = MCP::Client::HTTP.new(url: "https://mcp.example/rpc")
  MCP::Client.new(transport:)
end
end

Defined Under Namespace

Classes: CallbackFailure

Constant Summary collapse

UNSET =

:nodoc:

Object.new.freeze

Class Method Summary collapse

Methods included from Support::ClassAttributes

class_attribute, included

Class Method Details

.client(**connect_options, &factory) ⇒ Object

Declares the factory for a fresh, unconnected official MCP::Client. The factory receives the current Tool::Binding. LittleGhost forwards connect_options to MCP::Client#connect, then calls close on the returned transport when it exposes that method. Calling client without a block or options returns the inherited factory, if one is configured.

:call-seq:

client() -> Proc or nil
client(**connect_options) { |binding| ... } -> Proc

Raises:

  • (ArgumentError)


62
63
64
65
66
67
68
# File 'lib/little_ghost/mcp/toolset.rb', line 62

def client(**connect_options, &factory)
  return client_factory_value if !factory && connect_options.empty?
  raise ArgumentError, "client requires a factory block" unless factory

  self.connect_options_value = connect_options.dup.freeze
  self.client_factory_value = factory
end

.map_result(&mapping) ⇒ Object

Maps the value produced by LittleGhost's default result conversion. The block also receives the raw tools/call result: Hash, the official mcp_tool:, the submitted arguments:, and the current binding:. Return any Ruby value or Tool::Result. With no block, returns the inherited mapping, if one is configured.

:call-seq:

map_result() -> Proc or nil
map_result { |value, result:, mcp_tool:, arguments:, binding:| ... } -> Proc


95
96
97
98
99
# File 'lib/little_ghost/mcp/toolset.rb', line 95

def map_result(&mapping)
  return result_mapping_value unless mapping

  self.result_mapping_value = mapping
end

.map_tool(&mapping) ⇒ Object

Maps each generated Tool class before it is bound to the Agent. The block receives the generated class, the official MCP::Client::Tool as mcp_tool:, and the current binding:. Return the class, a subclass, or nil to omit it. Renaming the class does not change the operation name sent to the server. With no block, returns the inherited mapping, if one is configured.

:call-seq:

map_tool() -> Proc or nil
map_tool { |tool_class, mcp_tool:, binding:| ... } -> Proc


80
81
82
83
84
# File 'lib/little_ghost/mcp/toolset.rb', line 80

def map_tool(&mapping)
  return tool_mapping_value unless mapping

  self.tool_mapping_value = mapping
end

.on_error(&callback) ⇒ Object

Observes an expected discovery failure caught by optional true. The block receives the translated LittleGhost error and the current binding:. Exceptions raised by the block propagate. With no block, returns the inherited callback, if one is configured.

:call-seq:

on_error() -> Proc or nil
on_error { |error, binding:| ... } -> Proc


123
124
125
126
127
# File 'lib/little_ghost/mcp/toolset.rb', line 123

def on_error(&callback)
  return error_callback_value unless callback

  self.error_callback_value = callback
end

.optional(value = UNSET) ⇒ Object

Makes expected provider and protocol discovery failures produce no tools. Configuration, cancellation, deadline, and callback failures still propagate. With no argument, reports whether discovery is optional.

:call-seq:

optional() -> true or false
optional(value) -> true or false


109
110
111
112
113
# File 'lib/little_ghost/mcp/toolset.rb', line 109

def optional(value = UNSET)
  return optional_value if value.equal?(UNSET)

  self.optional_value = !!value
end

.tools(binding) ⇒ Object

Connects the configured client and returns Tool classes for binding. When the binding has a run, the run owns the shared client session. Otherwise, the ToolRegistry that resolves the returned classes closes the session through its generated Tool instances. A caller that bypasses ToolRegistry must instantiate and close a returned class.

Expected discovery failures return an empty Array when optional true. After the factory returns a client, later failures call close when its transport exposes that method; failures then propagate unless they are optional.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/little_ghost/mcp/toolset.rb', line 139

def tools(binding)
  context = binding.run&.context
  context&.check!
  official_client = build_client(binding)
  session = Session.new(official_client)
  connect_client(official_client, context:)
  binding.run&.register(session)

  Instrumentation.instrument(:mcp_discovery, toolset: toolset_name) do |telemetry|
    adapter = Adapter.new(
      client: official_client,
      session:,
      name: toolset_name,
      tool_mapper: wrapped_tool_mapper,
      result_mapper: wrapped_result_mapper
    )
    discovered = adapter.tools(context:, binding:)
    session.close if discovered.empty? && !binding.run
    telemetry[:outcome] = :success
    telemetry[:tool_count] = discovered.length
    discovered
  end
rescue CallbackFailure => error
  session&.close
  raise error.original
rescue ProviderError, ProtocolError, ToolError => error
  session&.close
  raise unless optional_value

  error_callback_value&.call(error, binding:)
  []
rescue
  session&.close
  raise
end