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 trueidempotent_hint: Indicates if the tool’s operations are idempotent. Defaults to falseopen_world_hint: Indicates if the tool operates in an open world context. Defaults to trueread_only_hint: Indicates if the tool only reads data (doesn’t modify state). Defaults to falsetitle: 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.
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