Connect agents to MCP servers
Give an Agent access to operations published by a Model Context Protocol (MCP) server without changing how your application calls the Agent. LittleGhost::MCP::Toolset turns operations discovered by the official MCP Ruby client into LittleGhost Tool classes, which join the same tools declaration as local application Tools.
MCP support is opt-in. Requiring little_ghost alone does not install or load the SDK or its transports.
Connect over Streamable HTTP
Add the official SDK and its HTTP dependencies to your bundle:
gem "mcp", "~> 1.3" gem "faraday", "~> 2.0" gem "event_stream_parser", "~> 1.0"
Require the integration explicitly, then create a Toolset with a client block. Return a new official client from the block:
require "little_ghost/mcp" class HelpCenterTools < LittleGhost::MCP::Toolset client do |_binding| transport = MCP::Client::HTTP.new( url: "https://mcp.example/rpc" ) MCP::Client.new(transport:) end end class CustomerSupportAgent < LittleGhost::Agent system_prompt "Use help-center tools for published guidance." tools HelpCenterTools end run = CustomerSupportAgent.ask("How long do refunds take?") run.response
LittleGhost calls the block once for each Agent run. It connects the client, discovers the server’s Tools, and closes the transport when the run ends.
The client block receives the current Tool::Binding. Use it to build headers or credentials from authenticated application context without storing them on the Toolset:
class AccountTools < LittleGhost::MCP::Toolset client do |binding| token = McpAccessTokens.for_actor(binding.run.invocation.actor_id) transport = MCP::Client::HTTP.new( url: "https://mcp.example/rpc", headers: {"Authorization" => "Bearer #{token}"} ) MCP::Client.new(transport:) end end
Connect a local server over standard input and output
A standard input and output (stdio) connection needs only the official SDK:
gem "mcp", "~> 1.3"
Construct the SDK transport in the same client block:
class LocalDatabaseTools < LittleGhost::MCP::Toolset client do |_binding| transport = MCP::Client::Stdio.new( command: "bundle", args: ["exec", "database-mcp"], env: {"APP_ENV" => "production"}, read_timeout: 30 ) MCP::Client.new(transport:) end end
A stdio server is a child process with the same operating-system permissions as your Ruby process. It is not a sandbox. Use an operating-system sandbox when you do not fully trust the executable.
The env Hash changes only the variables you name. The child inherits every other variable from the parent process. Clear sensitive variables explicitly, or launch the server through an isolated wrapper when it must not receive ambient credentials.
Configure the official client
Set timeouts, message limits, OAuth, middleware, and other transport behavior through the SDK. Keyword arguments on client go directly to MCP::Client#connect; use them for an explicit protocol mode, version, or client capability.
Respond to server requests
A server may need more information while handling a Tool call. MCP calls this elicitation. Advertise the capability when connecting, then register a handler on the client:
class InteractiveTools < LittleGhost::MCP::Toolset client(capabilities: {elicitation: {}}) do |binding| transport = MCP::Client::HTTP.new(url: "https://mcp.example/rpc") MCP::Client.new(transport:).tap do |official_client| official_client.on_elicitation do |request| Elicitations.answer(request, run: binding.run) end end end end
The handler can answer from the current Run or pass the request to an application interface. Sampling similarly lets a server request a model call, while roots tell a server which directories the application makes available. Configure these features on the official client.
Treat every server-initiated handler request as untrusted. Authorize it against the current run, restrict roots to intended paths, constrain model sampling and its cost, and return only the application data that server is allowed to receive.
Return a new, unconnected client for every run. Building it should not start remote work. LittleGhost closes transports that provide close, including when connection or discovery fails. If a custom transport owns other resources, it must release them itself.
Set transport timeouts to match the application’s deadline. LittleGhost passes Run cancellation and deadlines to Tool discovery and calls, but connection has its own timeout behavior.
Plan concurrency and cancellation
Most applications only need transport timeouts. The details below matter when the application uses a Fiber scheduler or runs independent Tools concurrently.
Connection
LittleGhost connects while it discovers the Agent’s Tools. Run cancellation does not interrupt connection, so the transport’s connection and read timeouts control how long it can wait. Choose a scheduler-compatible transport adapter when other fibers must continue during that time.
Tool discovery and calls
Tool discovery and calls honor Run cancellation and deadlines. Cancelling an HTTP call returns control to the Run, but the underlying request may continue waiting until the server responds or the transport closes.
All Tools for one run share the same client. HTTP calls may overlap when LittleGhost runs independent Tools concurrently, so choose a Faraday adapter that supports overlapping calls. If the server or transport requires one call at a time, mark the generated Tools exclusive:
map_tool do |tool_class, mcp_tool:, binding:| tool_class.exclusive true tool_class end
LittleGhost serializes calls through the official stdio transport. If one of those calls is cancelled, it closes that run’s session rather than risk reading a late response as the answer to a later call. A custom or decorated stdio transport must provide its own serialization, cancellation behavior, and cleanup.
Server handlers
The server may invoke a handler while other work is active. Write handlers for concurrent use and capture the application values they need when building the client. Do not rely on fiber-local state inside a handler.
Select and map Tools
By default, the Agent receives every Tool published by the server. LittleGhost normalizes each name for the model and keeps the server’s original name for calls. Discovery stops after 1,000 Tools or 100 pages, which bounds the work an untrusted catalog can create in one run.
Use map_tool to omit operations or configure their generated classes:
class CuratedHelpCenterTools < HelpCenterTools map_tool do |tool_class, mcp_tool:, binding:| next unless %w[search fetch].include?(mcp_tool.name) tool_class.tool_name "help_center_#{mcp_tool.name}" tool_class end end
mcp_tool is the Tool published by the official client. Return the generated class, a subclass, or nil to omit it. Renaming the generated Tool changes the name shown to the model, not the name sent back to the server.
LittleGhost sends the server’s input schema to the model unchanged. It does not validate arguments against that schema; the server must validate them before performing an operation. LittleGhost limits schema depth and node count during discovery. Configure the transport’s message-size limit to bound the bytes accepted from a server.
Safety note: Server descriptions and results become visible to the model. Treat them as untrusted content. Expose only the operations the Agent needs, use narrowly scoped credentials, and have the server authorize every sensitive call.
Convert results
Without a mapping, LittleGhost returns structuredContent when the server sends it. Otherwise, it returns the text or remaining visible content blocks. MCP image blocks become Artifacts.
Suppose the help-center server returns structuredContent with an articles array. Use map_result to present each article as a short line:
map_result do |value, mcp_tool:, **| next value unless mcp_tool.name == "search" value.fetch("articles").map do |article| "#{article.fetch("title")}: #{article.fetch("url")}" end end
value is LittleGhost’s default conversion, and mcp_tool identifies the server operation. Return value unchanged or replace it with any Ruby value or Tool::Result. The block can also receive the raw result, sent arguments, and current binding; see the MCP::Toolset API for their exact shapes.
Image Artifacts remain attached to the mapped result. A server result marked isError remains a model-visible Tool error.
For custom images, files, or deferred Artifacts, continue with Structured Results and Content. A deferred resolver must verify ownership, fetch only from the intended service, and limit response size.
Keep an optional server from blocking a run
An optional server can fail discovery without preventing Agent construction:
class OptionalHelpCenterTools < HelpCenterTools optional true on_error do |error, binding:| McpAvailability.report(error, run_id: binding.run.invocation.run_id) end end
optional true turns an expected connection or protocol error during discovery into an empty Tool set. Configuration errors, cancellation, deadlines, and failures in application callbacks still stop the run.
Let the SDK negotiate the protocol version
The official MCP Ruby SDK handles protocol negotiation and transport behavior. Without explicit connect options, it negotiates with the server using a protocol version the installed SDK supports. Pin protocol_version or mode only when interoperability requires it.
Need sampling, roots, elicitation, or another client capability? Configure it on the client you build above. The installed SDK and negotiated protocol version determine what is available. Check the Ruby SDK client documentation and the versioned MCP specification for the capability you need.
Continue with Tools for the local Tool boundary, Integrations to send Runs through AG-UI or OpenTelemetry, or the MCP::Toolset API for exact mapping and ownership contracts.