MCP Ruby SDK

repository·main·Indexed 21 days ago

https://github.com/modelcontextprotocol/ruby-sdk

The official Ruby implementation of the Model Context Protocol (MCP), providing tools to build MCP servers and clients. It supports multiple transport layers, including stdio and Streamable HTTP with Server-Sent Events (SSE), and includes specific integration patterns for Ruby on Rails.

Tokens
43.9K
Snippets
127
Records
166
Agent score
72%

What's inside modelcontextprotocol-ruby-sdk

  1. Key features of the MCP Ruby SDK

    main

    The MCP Ruby SDK provides the following capabilities for building LLM-integrated applications:

    • Protocol Implementation: Full JSON-RPC 2.0 message handling, including protocol initialization and capability negotiation.
    • MCP Primitive Support: Registration and invocation of Tools, Prompts, and Resources.
    • Transport Layers: Support for Stdio and Streamable HTTP (including Server-Sent Events/SSE).
    • Client Capabilities: Support for communicating with MCP servers.
    • Advanced Protocol Features: Notifications, sampling, progress tracking, and completions.
  2. Understand the MCP Ruby SDK API stability and versioning

    main
    The SDK follows Semantic Versioning. The public API is currently considered pre-stable. The 1.0.0 release will mark the public API as stable, meaning breaking changes will only be introduced in major releases (following the policy in VERSIONING.md).
  3. Define MCP Prompts

    main

    Prompts are templates for LLM interactions. They can be defined via Class Definition (subclassing MCP::Prompt) or Server-level Definition (server.define_prompt). Prompts use arguments to define required or optional inputs and a template (or block) to return an MCP::Prompt::Result containing messages.

    # Class Definition
    class CodeReviewPrompt < MCP::Prompt
      prompt_name "code_review"
      description "Review code for best practices"
      arguments [
        MCP::Prompt::Argument.new(name: "code", description: "Code to review", required: true),
      ]
    
      class << self
        def template(args, server_context:)
          MCP::Prompt::Result.new(
            description: "Code review",
            messages: [
              MCP::Prompt::Message.new(
                role: "user",
                content: MCP::Content::Text.new("Please review this code:\n#{args[:code]}")
              ),
            ]
          )
        end
      end
    end
    
    # Server-level Definition
    server.define_prompt(
      name: "code_review",
      description: "Review code for best practices",
      arguments: [
        MCP::Prompt::Argument.new(name: "code", description: "Code to review", required: true),
      ]
    ) do |args, server_context:|
      MCP::Prompt::Result.new(
        description: "Code review",
        messages: [
          MCP::Prompt::Message.new(
            role: "user",
            content: MCP::Content::Text.new("Please review this code:\n#{args[:code]}")
          ),
        ]
      )
    end
  4. How Streamable HTTP Transport works

    main

    The Streamable HTTP transport follows a specific lifecycle for managing client-server communication:

    1. Initialize Session: The client sends a POST request with the initialize method. The server responds with a unique session ID in the Mcp-Session-Id header.
    2. Establish SSE Connection (Optional): The client sends a GET request with the Mcp-Session-Id header to open a Server-Sent Events stream for receiving notifications.
    3. Send Requests: The client sends subsequent POST requests using the JSON-RPC 2.0 format, including the Mcp-Session-Id header.
    4. Close Session: The client sends a DELETE request with the Mcp-Session-Id header to terminate the session.
  5. Important Deployment Notes for Streamable HTTP in Rails

    main

    When using StreamableHTTPTransport in a Rails environment, be aware of the following constraints:

    • Process Affinity: The transport keeps session and SSE state in memory. It must run in a single process. If using Puma, do not enable clustered mode (workers 0 is default).
    • Load Balancing: If running multiple instances behind a load balancer, you must use sticky sessions keyed on the Mcp-Session-Id header, or configure the transport with stateless: true.
    • Code Reloading: In the 'Rails (mount)' pattern, the server and transport are built once at boot in config/routes.rb. Consequently, config.enable_reloading = false is required. You must restart the server after changing tool or resource code.
    • Autoloading: Tool classes cannot be referenced from config/initializers because Zeitwerk has not set up autoloading at that stage. However, they can be referenced in routes as routes load late enough.
  6. Identify breaking changes in the MCP Ruby SDK

    main

    A breaking change is any modification that requires you to change your code to maintain compatibility after an upgrade. This includes:

    • Removing or renaming a public class, module, method, or constant.
    • Changing a public method's signature such that it rejects previously valid arguments.
    • Changing documented behavior that users depend on.

    Additive changes (new classes, new methods, or new optional keyword arguments) are not breaking changes.

    Exceptions to the rule: Incompatible changes may be introduced in a minor release if they are necessary to:

    1. Fix incorrect conformance to the MCP specification.
    2. Address a security vulnerability.
    3. Fix a clear defect (e.g., crashes, data corruption, or behavior contradicting documentation) where preserving compatibility would mean preserving the defect.
  7. Send Server Notifications

    main

    The server can notify clients about changes to tools, prompts, or resources to avoid polling.

    Notification Scoping

    • Session-scoped: report_progress and notify_log_message called via server_context inside a tool handler are sent only to the requesting client.
    • Broadcast: notify_tools_list_changed, notify_prompts_list_changed, and notify_resources_list_changed are sent to all connected clients. These must be called on the server instance directly.

    Available Methods

    • server.notify_tools_list_changed
    • server.notify_prompts_list_changed
    • server.notify_resources_list_changed
    • server.notify_log_message (via server_context)
  8. Define MCP Tools

    main

    Tools provide functionality to LLM applications. You can define them using three patterns:

    1. Class Definition: Subclass MCP::Tool and implement a self.call method. This is the most robust way to define metadata like title, description, and input_schema.
    2. Block Definition: Use MCP::Tool.define(name: ..., description: ...) do ... end for a more concise functional approach.
    3. Server-level Definition: Use server.define_tool(name: ..., description: ...) do ... end to register a tool directly on an existing server instance.

    Note on Arguments: Tool arguments are delivered as a Hash with symbol keys at every nesting level. When accessing nested objects, use payload[:key] instead of payload["key"].

    # Class Definition
    class MyTool < MCP::Tool
      title "My Tool"
      description "This tool performs specific functionality..."
      input_schema(
        properties: {
          message: { type: "string" },
        },
        required: ["message"]
      )
      annotations(
        read_only_hint: true,
        destructive_hint: false,
      )
    
      def self.call(message:, server_context:)
        MCP::Tool::Response.new([{ type: "text", text: "OK" }])
      end
    end
    
    # Block Definition
    tool = MCP::Tool.define(
      name: "my_tool",
      description: "This tool performs specific functionality...",
    ) do |args, server_context:|
      MCP::Tool::Response.new([{ type: "text", text: "OK" }])
    end
    
    # Server-level Definition
    server = MCP::Server.new
    server.define_tool(
      name: "my_tool",
      description: "This tool performs specific functionality...",
    ) do |args, server_context:|
      MCP::Tool::Response.new([{ type: "text", text: "OK" }])
    end
  9. Understand the MCP Ruby SDK versioning scheme

    main

    The MCP Ruby SDK follows Semantic Versioning 2.0.0 (MAJOR.MINOR.PATCH).

    • MAJOR: Incremented for incompatible changes to the public API.
    • MINOR: Incremented for backwards-compatible new functionality.
    • PATCH: Incremented for backwards-compatible bug fixes.

    Note on the 0.x Phase: While the SDK is in the 0.x phase, the public API is considered unstable. Minor releases (0.x to 0.y) may contain breaking changes. Always check the CHANGELOG.md before upgrading during this phase.

  10. Access tool arguments as symbolized Hashes

    main
    When implementing a tool, arguments arrive as a Hash with symbol keys at every nesting level. This is because the transports parse JSON using symbolize_names: true. Always use symbol keys (e.g., payload[:subject]) instead of string keys (e.g., payload["subject"]) to read nested objects.
  11. Use server_context to pass request-specific data

    main

    The server_context is a user-defined hash passed into the MCP::Server instance. It is made available to all tool and prompt calls via the server_context keyword argument. This is ideal for providing contextual information like authentication state, user IDs, or request-specific metadata.

    Important: To ensure that the protocol's _meta parameter is correctly merged into your context, server_context must be a Hash (or nil). If you assign a non-Hash value, _meta will not be accessible.

    server = MCP::Server.new(
      name: "my_server",
      server_context: { user_id: current_user.id, request_id: request.uuid }
    )
  12. Implement Custom Transports with Cancellation support

    main

    To support the MCP cancellation specification in a custom transport, you must:

    1. Implement send_notification(notification:) to allow notifications/cancelled to be delivered.
    2. Accept the optional block passed to send_request(request:, &on_sent).
    3. Call the &on_sent block once the request bytes have been handed off to the wire.

    The cancel-dispatch thread waits for this &on_sent signal before issuing the notifications/cancelled message. If the block is not invoked, the SDK falls back to waiting for the worker thread to terminate, which ensures wire-order but delays the cancellation notification.