RubyLLM::MCP

repository·main·Indexed 42 days ago

https://github.com/patvice/ruby_llm-mcp

A Ruby client for the Model Context Protocol (MCP) that integrates with the RubyLLM ecosystem. It enables the use of MCP tools, resources, and prompts within Ruby-based LLM workflows via stdio, SSE, or streamable HTTP transports. Features include Rails integration generators, OAuth 2.1 support for CLI tools, and a flexible elicitation handling system for managing server requests for user input.

Tokens
53.9K
Snippets
195
Records
231
Agent score
53%

What's inside RubyLLM::MCP

  1. Overview of MCP Client capabilities

    main

    An MCP client manages local resources and provides specific capabilities to connected MCP servers. Key capabilities include:

    • Sampling: Allows servers to offload LLM requests to your client, leveraging your existing LLM connections and configurations.
    • Elicitation: Enables the client to handle structured follow-up questions, clarifications, or schema-validated data requests initiated by a server.
    • Roots: Provides servers with controlled filesystem access to specific directories, allowing them to understand project structures.
    • Transports: Manages the communication protocol between the client and the servers (supports built-in or custom implementations).
    • Extensions: Allows the client to advertise optional capabilities, such as support for MCP Apps, and negotiate protocol extensions.
  2. How MCP Sampling works

    main

    When sampling is enabled, an MCP server can send a "sample" request to your client. The client then:

    1. Executes the LLM request using your configured model.
    2. Returns the response back to the server.
    3. Allows the server to continue its processing.

    This pattern allows servers to maintain complex logic and workflows while leveraging the client's existing LLM infrastructure.

  3. Use SamplingHandler classes for advanced logic

    main

    For complex sampling requirements, use handler classes by inheriting from RubyLLM::MCP::Handlers::SamplingHandler. This is preferred over block-based callbacks for better testability, reusability, and composability.

    Key features of handler classes:

    • Guards: Use guard :method_name to define validation logic that must return true or an error string.
    • Options: Define configurable parameters using option :name, default: value.
    • Hooks: Use before_execute and after_execute blocks for lifecycle management (e.g., logging, metrics).
    • Execution: Implement the execute method, using default_chat_completion(model) to generate responses and accept(response) or reject(reason) to return the result to the server.
    class MySamplingHandler < RubyLLM::MCP::Handlers::SamplingHandler
      guard :check_message_length
    
      option :default_model, default: "gpt-4"
    
      before_execute do
        logger.info("Starting sampling")
      end
    
      def execute
        model = sample.model_preferences.model || options[:default_model]
        response = default_chat_completion(model)
        accept(response)
      end
    
      private
    
      def check_message_length
        return true if sample.message.length < 10_000
        "Message too long"
      end
    end
    
    # Register the handler with the client
    client.on_sampling(MySamplingHandler, default_model: "gpt-3.5-turbo")
  4. Understand MCP response patterns

    main

    RubyLLM MCP utilizes three distinct patterns for handling interactions with MCP servers:

    1. Synchronous response: Used for immediate return values from requests, such as tool.execute.
    2. Notification-driven async updates: Used for real-time server-sent updates during execution, such as logging, progress, or resource updates.
    3. Task lifecycle response: Used for long-running, pollable background work via tasks/get, tasks/result, and optional cancellation.

    Guidance: Use synchronous responses for short operations, notifications for real-time status, and tasks for long-running workflows.

  5. Accept, reject, or cancel elicitation requests

    main

    Elicitation handlers can return different outcomes to signal the result of the request to the MCP server:

    • Accept: Set elicitation.structured_response and return true.
    • Reject: Return false to indicate you do not want to provide the requested information.
    • Cancel: If the structured_response does not match the requested schema, the client automatically triggers a cancel. You can also handle errors or validation failures by returning false or using explicit cancellation logic.

    For asynchronous workflows, you can return :pending, use create_promise, or use defer to return an AsyncResponse. Deferred requests can be finished later using RubyLLM::MCP::Handlers::ElicitationRegistry.complete or .cancel.

    # Synchronous outcomes
    accept({ "confirmed" => true })  # => { action: :accept, response: {...} }
    reject("User declined")          # => { action: :reject, reason: "User declined" }
    cancel("Cancelled by user")      # => { action: :cancel, reason: "Cancelled by user" }
    
    # Async options
    :pending                         # Store in registry for later completion
    create_promise                   # Resolve/reject later
    defer                            # Return AsyncResponse
  6. Complex parameter support is now enabled by default

    main

    In version 0.7.x, MCP tools support complex parameters (such as arrays and nested objects) by default.

    Deprecation Warning: The support_complex_parameters! method is deprecated and is scheduled for removal in version 0.8.0. You should remove this call from your configuration block to prepare for future updates.

    # Remove this deprecated configuration in version 0.7.x
    RubyLLM::MCP.configure do |config|
      config.support_complex_parameters!
    end
  7. How extensions work in RubyLLM MCP

    main

    Extensions allow an MCP client to advertise optional capabilities to servers via the capabilities.extensions field. The system relies on three core components:

    1. Extension Registry: Handles the canonicalization of IDs and the merging of global and per-client configuration values.
    2. Configuration API: Provides methods like config.extensions.register(...) and config.extensions.enable_apps(...) to define capabilities.
    3. Adapter behavior: Determines whether the extension capabilities are actually advertised to the server based on the adapter type and protocol version.

    Extension IDs are normalized. For example, the alias io.modelcontextprotocol/apps is automatically converted to the canonical ID io.modelcontextprotocol/ui before being advertised outbound.

  8. Implement Human-in-the-Loop tool approvals

    main

    To prevent unauthorized actions, you can implement human-in-the-loop (HITL) workflows by defining handler classes that inherit from RubyLLM::MCP::Handlers::HumanInTheLoopHandler. These handlers can be configured globally or per-client.

    Handlers can use guard methods to perform pre-execution checks. They return one of three structured decisions:

    • approve: Allows execution.
    • deny("reason"): Blocks execution with a reason.
    • defer(timeout: seconds): Pauses execution to wait for an external decision (e.g., via a websocket or UI).
    class SafeToolHandler < RubyLLM::MCP::Handlers::HumanInTheLoopHandler
      option :safe_tools, default: ["read_file", "list_files"]
    
      def execute
        if options[:safe_tools].include?(tool_name)
          approve
        else
          deny("Tool '#{tool_name}' requires explicit approval")
        end
      end
    end
    
    # Per-client configuration
    client.on_human_in_the_loop(SafeToolHandler, safe_tools: ["read_file"])
  9. Choosing between :ruby_llm and :mcp_sdk adapters

    main

    When deciding which adapter to use for your MCP client, consider these guidelines:

    1. Use :ruby_llm if you want full feature support, need custom transports, or are unsure which to pick. It is the default adapter.
    2. Use :mcp_sdk if you specifically require the official MCP implementation.
      • Requirement: Requires Ruby 3.1+.
      • Requirement: You must add gem 'mcp', '~> 0.7' to your Gemfile.
    3. Check feature requirements: Some advanced client features are only available via the :ruby_llm adapter.
  10. Select an SDK adapter

    main

    RubyLLM MCP supports two adapters. You can set a global default using config.default_adapter = :adapter_name.

    • :ruby_llm (default): Full MCP protocol implementation. Supports all transport types (stdio, SSE, HTTP) and advanced features like sampling, roots, and progress tracking.
    • :mcp_sdk: Official Anthropic-maintained SDK. Supports core features (tools, resources, prompts) but has limited transport support (stdio, HTTP; no SSE).
    RubyLLM::MCP.configure do |config|
      # Options: :ruby_llm (default), :mcp_sdk
      config.default_adapter = :ruby_llm
    end
  11. Configure Filesystem Roots

    main

    Provide MCP servers with access to specific filesystem paths using roots. You can configure these globally or modify them on a per-client basis at runtime.

    # Global configuration
    RubyLLM::MCP.configure do |config|
      config.roots = ["/path/to/project", Rails.root]
    end
    
    # Runtime client manipulation
    client = RubyLLM::MCP.client(...)
    puts client.roots.paths
    
    client.roots.add("/new/path")
    client.roots.remove("/old/path")
  12. Implement the Render/Action Loop pattern

    main

    For predictable MCP App behavior, follow the Render/Action split pattern:

    1. Render: Use a specific tool or resource to provide embeddable UI payloads (such as HTML or URI-based references).
    2. Action: Use separate, dedicated tools for data mutations (e.g., create, toggle, mark_done).
    3. Loop: Route UI events back to the mutation tools, then re-fetch or patch the UI state to reflect changes.