ruby-sdk/docs/_server/tools.md
Koichi ITO 88831b46f6 Move the Documentation From README.md to the Documentation Site
Move the documentation that had grown to 3000+ lines in README.md onto https://ruby.sdk.modelcontextprotocol.io
and slim the README down to a quick start and feature overview, following the Python SDK layout:

- Add 21 server pages as the docs/_server/ collection (overview plus transports, discovery, tools, prompts, resources,
  roots, sampling, elicitation, multi round-trip results, notifications, notification subscriptions, cancellation, progress,
  ping, completions, logging, pagination, server context, configuration, and custom methods), 8 client pages as
  the docs/_client/ collection (overview, transports, lifecycle, multi round-trip results, cancellation, ping, pagination,
  and authorization), and 3 extension pages as the docs/_extensions/ collection (overview, capability extensions, and MCP Apps,
  following the Extensions Overview recommendation that SDK documentation list the supported extensions) -
  one page per topic, ordered to match the sidebar of the 2026-07-28 specification, with the client-side APIs
  (pinging, cancelling, and paginating from MCP::Client) documented under Building Clients
- Add top-level Examples and Protocol Versions pages after Installation, linking the runnable examples in examples/
  and summarizing the supported protocol versions, the era model, and client negotiation
- Render the three sections as just-the-docs collections, which list every page beneath a plain category heading in
  the sidebar with no folding, and serve every page at an extensionless URL under /server/, /client/, and /extensions/,
  matching the sibling SDK documentation sites
- Replace docs/building-servers.md and docs/building-clients.md with redirects to the new section overview pages via
  jekyll-redirect-from, redirect the previously published /installation.html to its extensionless URL, fold their content
  that was missing from README.md into the new pages, and update the Tool argument keys reference comment in lib/mcp/server.rb to
  the relocated Tools page
- Reduce README.md to the badges, a compact feature overview in the Python SDK style, installation instructions, a stdio
  server and client quick start, and a License section, using absolute URLs for the remaining repository links
  so they resolve on rubygems.org
- Adapt formatting where GitHub rendering habits break on the site: convert the numbered "three ways to define" lists into headings,
  since kramdown restarts numbering at 1 when code blocks split list items, convert GitHub-style alerts into just-the-docs callouts
  with the SEP-2260 server-to-client association note raised to a red warning, merge the duplicated Exception Reporting
  and Configuration Block Data sections into the Configuration page, and relocate the Streamable HTTP settings that were nested under
  the Logging section into the Transports page
- Refresh the migrated content against the current implementation: correct stale claims and broken samples the README carried,
  note on each session-era feature how it relates to the modern lifecycle of MCP 2026-07-28, and point spec links at the latest revision,
  keeping deliberate 2025-11-25 pins for pages the modern revision removed
- Style the site after the Rails API documentation palette (red links and accents on neutral surfaces) with matching
  light and dark color schemes, center the sidebar and content as one block, add a Previous/Next footer pager following
  the sidebar order, open external links in a new tab, and serve the MCP logo as the favicon

Every code block and heading from the previous README was verified to have a home in the new docs pages or the slimmed README before
the reduction; a few samples were corrected rather than copied, as noted above.
2026-08-21 13:24:04 +09:00

13 KiB

layout title nav_order
default Tools 4

Tools

MCP spec includes Tools which provide functionality to LLM apps.

Defining Tools

This gem provides a MCP::Tool class that can be used to create tools in three ways.

1. As a class definition

Subclass MCP::Tool and declare the metadata with class-level helpers; call implements the tool:

class MyTool < MCP::Tool
  title "My Tool"
  description "This tool performs specific functionality..."
  input_schema(
    properties: {
      message: { type: "string" },
    },
    required: ["message"]
  )
  output_schema(
    properties: {
      result: { type: "string" },
      success: { type: "boolean" },
      timestamp: { type: "string", format: "date-time" }
    },
    required: ["result", "success", "timestamp"]
  )
  annotations(
    read_only_hint: true,
    destructive_hint: false,
    idempotent_hint: true,
    open_world_hint: false,
    title: "My Tool"
  )

  def self.call(message:, server_context:)
    MCP::Tool::Response.new([{ type: "text", text: "OK" }])
  end
end

tool = MyTool

2. Using the MCP::Tool.define method

MCP::Tool.define builds a tool from keyword arguments, with the block as its implementation:

tool = MCP::Tool.define(
  name: "my_tool",
  title: "My Tool",
  description: "This tool performs specific functionality...",
  annotations: {
    read_only_hint: true,
    title: "My Tool"
  }
) do |args, server_context:|
  MCP::Tool::Response.new([{ type: "text", text: "OK" }])
end

3. Using the MCP::Server#define_tool method

MCP::Server#define_tool registers the tool directly on a server instance:

server = MCP::Server.new
server.define_tool(
  name: "my_tool",
  description: "This tool performs specific functionality...",
  annotations: {
    title: "My Tool",
    read_only_hint: true
  }
) do |args, server_context:|
  Tool::Response.new([{ type: "text", text: "OK" }])
end

The server_context parameter is the server_context passed into the server and can be used to pass per request information, e.g. around authentication state.

Tool argument keys

Tool arguments are delivered as a Hash whose keys are Ruby symbols at every nesting level, including nested objects and objects inside arrays. The transports parse incoming JSON with JSON.parse(..., symbolize_names: true), so by the time a tool runs, a wire payload such as {"payload": {"subject": "greet"}} arrives as { payload: { subject: "greet" } }.

This means top-level values are bound through keyword arguments (def call(message:, payload: nil, server_context:)), and nested objects must be read with symbol keys:

class ExampleTool < MCP::Tool
  description "Echoes a nested argument"
  input_schema(
    properties: {
      message: { type: "string" },
      payload: {
        type: "object",
        properties: {
          subject: { type: "string" },
        }
      }
    },
    required: ["message"]
  )

  def self.call(message:, payload: nil, server_context:)
    subject = payload && payload[:subject] # symbol key, not payload["subject"]
    MCP::Tool::Response.new([{
      type: "text",
      text: "Message: #{message}; subject: #{subject}"
    }])
  end
end

Reading a nested value with a string key (payload["subject"]) returns nil. This is a Ruby-specific contract: Top-level keyword arguments require symbol keys, and parsing JSON with symbolize_names: true symbolizes nested objects too.

Calling a tool directly in a test with MyTool.call(payload: { "subject" => "greet" }, server_context: nil) passes string keys that a transport never delivers, so string-key access can pass tests yet fail against a real client. Exercise a tool under the delivered shape by round-tripping the arguments through JSON the same way a transport does:

delivered = JSON.parse(JSON.generate(arguments), symbolize_names: true)
MyTool.call(**delivered, server_context: nil)

Tool Annotations

Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:

  • destructive_hint: Indicates if the tool performs destructive operations. Defaults to true
  • idempotent_hint: Indicates if the tool's operations are idempotent. Defaults to false
  • open_world_hint: Indicates if the tool operates in an open world context. Defaults to true
  • read_only_hint: Indicates if the tool only reads data (doesn't modify state). Defaults to false
  • title: A human-readable title for the tool

Annotations can be set either through the class definition using the annotations class method or when defining a tool using the define method.

{: .note }

This Tool Annotations feature is supported starting from protocol_version: '2025-03-26'.

Tool Output Schemas

Tools can optionally define an output_schema to specify the expected structure of their results. This works similarly to how input_schema is defined and can be used in three ways.

1. Class definition with output_schema

Declare the schema with the output_schema class helper, alongside input_schema:

class WeatherTool < MCP::Tool
  tool_name "get_weather"
  description "Get current weather for a location"

  input_schema(
    properties: {
      location: { type: "string" },
      units: { type: "string", enum: ["celsius", "fahrenheit"] }
    },
    required: ["location"]
  )

  output_schema(
    properties: {
      temperature: { type: "number" },
      condition: { type: "string" },
      humidity: { type: "integer" }
    },
    required: ["temperature", "condition", "humidity"]
  )

  def self.call(location:, units: "celsius", server_context:)
    # Call weather API and structure the response
    api_response = WeatherAPI.fetch(location, units)
    weather_data = {
      temperature: api_response.temp,
      condition: api_response.description,
      humidity: api_response.humidity_percent
    }

    output_schema.validate_result(weather_data)

    MCP::Tool::Response.new([{
      type: "text",
      text: weather_data.to_json
    }])
  end
end

2. Using Tool.define with output_schema

Pass the schema as the output_schema: keyword argument:

tool = MCP::Tool.define(
  name: "calculate_stats",
  description: "Calculate statistics for a dataset",
  input_schema: {
    properties: {
      numbers: { type: "array", items: { type: "number" } }
    },
    required: ["numbers"]
  },
  output_schema: {
    properties: {
      mean: { type: "number" },
      median: { type: "number" },
      count: { type: "integer" }
    },
    required: ["mean", "median", "count"]
  }
) do |args, server_context:|
  # Calculate statistics and validate against schema
  MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
end

3. Using OutputSchema objects

Construct an MCP::Tool::OutputSchema object explicitly:

class DataTool < MCP::Tool
  output_schema MCP::Tool::OutputSchema.new(
    properties: {
      success: { type: "boolean" },
      data: { type: "object" }
    },
    required: ["success"]
  )
end

Output schema may also describe an array of objects:

class WeatherTool < MCP::Tool
  output_schema(
    type: "array",
    items: {
      properties: {
        temperature: { type: "number" },
        condition: { type: "string" },
        humidity: { type: "integer" }
      },
      required: ["temperature", "condition", "humidity"]
    }
  )
end

Please note: in this case, you must provide type: "array". The default type for output schemas is object, applied only when the schema declares no root keyword (type, $ref, oneOf, anyOf, allOf, not, if, const, enum).

Per SEP-2106, an output schema may be any valid JSON Schema 2020-12 document, including a primitive root ({ type: "string" }) or a root-level composition:

class FlexibleTool < MCP::Tool
  output_schema(
    oneOf: [
      { type: "string" },
      { type: "array", items: { type: "number" } }
    ]
  )
end

Input schemas keep type: "object" at the root but accept the full 2020-12 vocabulary below it ($defs/$ref, oneOf/anyOf/allOf/not, if/then/else). Two resource bounds apply to all tool schemas: only same-document $refs (starting with #) are accepted, and documents are capped at MCP::Tool::Schema::MAX_SCHEMA_DEPTH nesting levels and MCP::Tool::Schema::MAX_SUBSCHEMA_COUNT subschema objects; violations raise ArgumentError at construction time.

MCP spec for the Output Schema specifies that:

  • Server Validation: Servers MUST provide structured results that conform to the output schema
  • Client Validation: Clients SHOULD validate structured results against the output schema
  • Better Integration: Enables strict schema validation, type information, and improved developer experience
  • Backward Compatibility: Tools returning structured content SHOULD also include serialized JSON in a TextContent block

The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.

By default, server-side validation of tool results against output_schema is disabled for backwards compatibility. To validate successful tool responses, enable validate_tool_call_results on the server configuration:

configuration = MCP::Configuration.new(validate_tool_call_results: true)
server = MCP::Server.new(
  name: "example_server",
  tools: [WeatherTool],
  configuration: configuration
)

When enabled, successful tool responses for tools with an output_schema must include structured_content that conforms to the schema. Error responses are not validated against the output schema.

Tool Responses with Structured Content

Tools can return structured data alongside text content using the structured_content parameter.

The structured content will be included in the JSON-RPC response as the structuredContent field.

Per SEP-2106, structured_content may be any JSON value, not only an object. When a tool returns a non-object value (e.g. an array) without providing any content blocks, the server automatically mirrors it into content as serialized JSON text so older clients that only read content still receive the data.

class WeatherTool < MCP::Tool
  description "Get current weather and return structured data"

  def self.call(location:, units: "celsius", server_context:)
    # Call weather API and structure the response
    api_response = WeatherAPI.fetch(location, units)
    weather_data = {
      temperature: api_response.temp,
      condition: api_response.description,
      humidity: api_response.humidity_percent
    }

    output_schema.validate_result(weather_data)

    MCP::Tool::Response.new(
      [{
        type: "text",
        text: weather_data.to_json
      }],
      structured_content: weather_data
    )
  end
end

Tool Responses with Errors

Tools can return error information alongside text content using the error parameter.

The error will be included in the JSON-RPC response as the isError field.

class WeatherTool < MCP::Tool
  description "Get current weather and return structured data"

  def self.call(server_context:)
    # Do something here
    content = {}

    MCP::Tool::Response.new(
      [{
        type: "text",
        text: content.to_json
      }],
      structured_content: content,
      error: true
    )
  end
end

Tool Responses with Image, Audio, and Embedded Resources

Tool responses are not limited to text. The MCP::Content module provides Image, Audio, and EmbeddedResource content types, which serialize to the image, audio, and resource content blocks defined by the MCP spec. Image and audio data is passed as a base64-encoded string together with its MIME type:

class ChartTool < MCP::Tool
  description "Render a chart as a PNG image"

  def self.call(server_context:)
    MCP::Tool::Response.new([
      MCP::Content::Text.new("Here is the rendered chart:").to_h,
      MCP::Content::Image.new(Base64.strict_encode64(render_chart_png), "image/png").to_h,
    ])
  end
end

class SpeechTool < MCP::Tool
  description "Synthesize speech audio"

  def self.call(server_context:)
    MCP::Tool::Response.new([
      MCP::Content::Audio.new(Base64.strict_encode64(synthesize_wav), "audio/wav").to_h,
    ])
  end
end

An embedded resource wraps MCP::Resource::TextContents or MCP::Resource::BlobContents, allowing a tool to return resource contents inline:

class ReportTool < MCP::Tool
  description "Return a report as an embedded resource"

  def self.call(server_context:)
    contents = MCP::Resource::TextContents.new(
      uri: "report://monthly",
      mime_type: "application/json",
      text: { total: 42 }.to_json,
    )

    MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(contents).to_h])
  end
end