Class: LittleGhost::MCP::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/mcp/client.rb

Overview

Client is the lower-level interface for loading tools from an MCP server. Most Agents can declare a reusable Toolset instead. Use Client directly when an application needs a custom transport.

transport = LittleGhost::MCP::HTTPTransport.new(url: "https://mcp.example/rpc")
client = LittleGhost::MCP::Client.new(transport:)
client.tools.map(&:tool_name) # => ["search", "fetch"]

Tool names are normalized and checked for collisions. LittleGhost limits catalog size, schema complexity, and returned media before creating Tool classes. tool_mapper and result_mapper use the same Definition, Result, and Call values as Toolset.

The server chooses its definitions and results. LittleGhost checks their structure before creating Tools, but the application still chooses which servers and operations an Agent may use.

A client and its transport represent one authenticated server session. Create a separate pair for each authenticated user or service identity. Protocol initialization and subsequent requests through one client are serialized; do not share its transport with another client.

Defined Under Namespace

Classes: PreparedDefinition

Constant Summary collapse

MAX_TOOL_NAME_LENGTH =

:nodoc:

64
ALIAS_DIGEST_LENGTH =

:nodoc:

12
DEFAULT_MAX_TOOLS =

:nodoc:

1_000
DEFAULT_MAX_PAGES =

:nodoc:

100
DEFAULT_MAX_DISCOVERY_BYTES =

:nodoc:

10 * 1024 * 1024
DEFAULT_MAX_DISCOVERY_NODES =

:nodoc:

100_000
DEFAULT_MAX_CURSOR_BYTES =

:nodoc:

16 * 1024
DEFAULT_MAX_DEFINITION_DEPTH =

:nodoc:

64
DEFAULT_MAX_DEFINITION_NODES =

:nodoc:

10_000
DEFAULT_MAX_SCHEMA_PATTERNS =

:nodoc:

1_000
DEFAULT_MAX_SCHEMA_PATTERN_BYTES =

:nodoc:

1024 * 1024
DEFAULT_MAX_SCHEMA_PATTERN_SOURCE_BYTES =

:nodoc:

64 * 1024
DEFAULT_MAX_IMAGES =

:nodoc:

20
DEFAULT_MAX_IMAGE_BYTES =

:nodoc:

16 * 1024 * 1024
DEFAULT_MAX_TOTAL_IMAGE_BYTES =

:nodoc:

64 * 1024 * 1024
ECMA_REGEXP_RESOLVER =

:nodoc:

lambda do |pattern| # :nodoc:
  source = JSONSchemer::EcmaRegexp.ruby_equivalent(pattern)
  Regexp.new(source, timeout: Tool::SchemaValidator::REGEXP_TIMEOUT)
end

Instance Method Summary collapse

Constructor Details

#initialize(transport:, name: "mcp", tool_mapper: nil, result_mapper: nil) ⇒ Client

Uses a transport that responds to send. tool_mapper receives each generated Tool class and may return a configured class or nil. result_mapper receives Result and Call values. Catalog, schema, and media limits use fixed framework defaults.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/little_ghost/mcp/client.rb', line 195

def initialize(
  transport:,
  name: "mcp",
  tool_mapper: nil,
  result_mapper: nil
)
  validate_callback(tool_mapper, :tool_mapper)
  validate_callback(result_mapper, :result_mapper)

  @transport = transport
  @name = String(name)
  @tool_mapper = tool_mapper
  @result_mapper = result_mapper
  @max_tools = DEFAULT_MAX_TOOLS
  @max_pages = DEFAULT_MAX_PAGES
  @max_discovery_bytes = DEFAULT_MAX_DISCOVERY_BYTES
  @max_discovery_nodes = DEFAULT_MAX_DISCOVERY_NODES
  @max_definition_depth = DEFAULT_MAX_DEFINITION_DEPTH
  @max_definition_nodes = DEFAULT_MAX_DEFINITION_NODES
  @max_images = DEFAULT_MAX_IMAGES
  @max_image_bytes = DEFAULT_MAX_IMAGE_BYTES
  @max_total_image_bytes = DEFAULT_MAX_TOTAL_IMAGE_BYTES
  @request_id = 0
  @mutex = Mutex.new
  @initialization_mutex = Mutex.new
  @definitions_mutex = Mutex.new
  @definitions_by_name = {}
  @initialized = false
end

Instance Method Details

#call(name, arguments, context: nil, binding: Tool::Binding.new) ⇒ Object

Calls a generated or named Tool and returns the common Tool execution result. Names discovered through #tools resolve to their stored Definition; an undiscovered name is treated as a source name.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/little_ghost/mcp/client.rb', line 246

def call(name, arguments, context: nil, binding: Tool::Binding.new)
  context&.check!
  ensure_initialized(context:)
  prepared = if name.is_a?(PreparedDefinition)
    name
  elsif name.is_a?(Definition)
    prepare_definition(name, context:)
  else
    definition_for_call(name, context:)
  end
  definition = prepared.definition
  call_value = Call.new(definition:, arguments:, context:, binding:)
  raw = request(
    "tools/call",
    {name: definition.source_name, arguments: call_value.arguments.to_h},
    context:
  )
  result = build_result(raw, prepared:)
  context&.check!
  mapped = if @result_mapper
    @result_mapper.call(result, call: call_value, binding:)
  else
    default_value(result)
  end
  context&.check!
  tool_result(mapped, protocol_result: result)
end

#tools(context: nil, binding: Tool::Binding.new) ⇒ Object

Negotiates the protocol when needed, then provides Tool classes for the server's current definitions.



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/little_ghost/mcp/client.rb', line 227

def tools(context: nil, binding: Tool::Binding.new)
  context&.check!
  ensure_initialized(context:)
  definitions = list_tool_definitions(context:).map do |raw|
    context&.check!
    build_definition(raw, context:).tap { context&.check! }
  end
  tools = definitions.filter_map do |definition|
    context&.check!
    build_tool(definition, binding:).tap { context&.check! }
  end
  index_tools!(tools, context:)
  context&.check!
  tools.freeze
end