Class: LittleGhost::MCP::Client

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

Overview

Client makes tools from an MCP server available as ordinary LittleGhost tools. Agents can use a remote capability without learning a second tool interface.

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

Tool names are normalized and checked for collisions. Pagination, tool count, and response sizes are limited; rejected_tools and definition_filter can enforce an application allowlist.

Server definitions and results remain untrusted input. LittleGhost validates them before creating tools, but applications still decide which servers and capabilities an agent may use.

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

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

Instance Method Summary collapse

Constructor Details

#initialize(transport:, name: "mcp", prefix: nil, rejected_tools: [], definition_filter: nil, max_tools: DEFAULT_MAX_TOOLS, max_pages: DEFAULT_MAX_PAGES) ⇒ Client

Uses a transport that responds to send.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/little_ghost/mcp/client.rb', line 173

def initialize(
  transport:,
  name: "mcp",
  prefix: nil,
  rejected_tools: [],
  definition_filter: nil,
  max_tools: DEFAULT_MAX_TOOLS,
  max_pages: DEFAULT_MAX_PAGES
)
  if definition_filter && !definition_filter.respond_to?(:call)
    raise ArgumentError, "definition_filter must respond to call"
  end

  @transport = transport
  @name = String(name)
  @prefix = prefix&.to_s
  @rejected_tools = rejected_tools.map(&:to_s).freeze
  @definition_filter = definition_filter
  @max_tools = positive_integer(max_tools, :max_tools)
  @max_pages = positive_integer(max_pages, :max_pages)
  @request_id = 0
  @mutex = Mutex.new
  @initialization_mutex = Mutex.new
  @source_names_mutex = Mutex.new
  @source_names = {}
  @initialized = false
end

Instance Method Details

#call(name, arguments, context: nil) ⇒ Object

Calls an exposed tool and produces text or serialized structured content. Server-declared errors raise ToolError.

Raises:



219
220
221
222
223
224
225
226
# File 'lib/little_ghost/mcp/client.rb', line 219

def call(name, arguments, context: nil)
  source_name = @source_names_mutex.synchronize { @source_names.fetch(name.to_s, name.to_s) }
  result = request("tools/call", {name: source_name, arguments: arguments}, context:)
  content = serialize_result(result)
  raise ToolError, content if result["isError"]

  content
end

#tools(context: nil) ⇒ Object

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



203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/little_ghost/mcp/client.rb', line 203

def tools(context: nil)
  @initialization_mutex.synchronize do
    initialize_protocol(context:) unless @initialized
  end
  definitions = list_tool_definitions(context:)
  definitions.filter_map do |definition|
    source_name = definition_name(definition)
    next if @rejected_tools.include?(source_name)
    next if @definition_filter && !@definition_filter.call(definition)

    build_tool(definition)
  end
end