Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/_client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ The client provides a wrapper class for tools returned by the server:
- `MCP::Client::Tool` - Represents a single tool with its metadata

This class provides easy access to tool properties like name, description, input schema, and output schema.
`title`, `annotations`, `icons`, `execution`, and `meta` (the tool's `_meta`) are kept as the server sent them,
so what an extension puts under `_meta` is readable: an MCP Apps host finds a tool's app under `tool.meta&.dig("ui", "resourceUri")`.
These values are untrusted input from the server: the SDK passes them through without acting on them,
and a host that renders `icons` or loads what `_meta` names treats them as it would any server-supplied URL or content.
4 changes: 4 additions & 0 deletions lib/mcp/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,14 @@ def list_tools(cursor: nil, meta: nil, cancellation: nil)

Tool.new(
name: tool["name"],
title: tool["title"],
description: tool["description"],
input_schema: tool["inputSchema"],
output_schema: tool["outputSchema"],
annotations: tool["annotations"],
icons: tool["icons"],
execution: tool["execution"],
meta: tool["_meta"],
)
end

Expand Down
10 changes: 8 additions & 2 deletions lib/mcp/client/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

module MCP
class Client
# A tool as `tools/list` described it. Every member is kept as the server sent it, so what an extension
# puts under `_meta` (MCP Apps names a tool's app under `_meta.ui`) reaches the host unchanged.
class Tool
attr_reader :name, :description, :input_schema, :output_schema, :annotations
attr_reader :name, :title, :description, :input_schema, :output_schema, :annotations, :icons, :execution, :meta

def initialize(name:, description:, input_schema:, output_schema: nil, annotations: nil)
def initialize(name:, description:, input_schema:, output_schema: nil, annotations: nil, title: nil, icons: nil, execution: nil, meta: nil)
@name = name
@title = title
@description = description
@input_schema = input_schema
@output_schema = output_schema
@annotations = annotations
@icons = icons
@execution = execution
@meta = meta
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions test/mcp/client/tool_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ def test_annotations_returns_nil_when_not_provided
assert_nil(@tool.annotations)
end

def test_title_icons_execution_and_meta_return_nil_when_not_provided
assert_nil(@tool.title)
assert_nil(@tool.icons)
assert_nil(@tool.execution)
assert_nil(@tool.meta)
end

def test_annotations_returns_annotations_when_provided
tool_with_annotations = Tool.new(
name: "test_tool_with_annotations",
Expand Down Expand Up @@ -68,17 +75,25 @@ def test_output_schema_returns_output_schema_when_provided
def test_initialization_with_all_parameters
tool = Tool.new(
name: "full_tool",
title: "Full Tool",
description: "A tool with all parameters",
input_schema: { "type" => "object" },
output_schema: { "type" => "object", "properties" => { "status" => { "type" => "boolean" } } },
annotations: { "readOnlyHint" => true },
icons: [{ "src" => "https://example.com/tool.png", "mimeType" => "image/png" }],
execution: { "taskSupport" => "optional" },
meta: { "ui" => { "resourceUri" => "ui://shop/plans" } },
)

assert_equal("full_tool", tool.name)
assert_equal("Full Tool", tool.title)
assert_equal("A tool with all parameters", tool.description)
assert_equal({ "type" => "object" }, tool.input_schema)
assert_equal({ "type" => "object", "properties" => { "status" => { "type" => "boolean" } } }, tool.output_schema)
assert_equal({ "readOnlyHint" => true }, tool.annotations)
assert_equal([{ "src" => "https://example.com/tool.png", "mimeType" => "image/png" }], tool.icons)
assert_equal({ "taskSupport" => "optional" }, tool.execution)
assert_equal({ "ui" => { "resourceUri" => "ui://shop/plans" } }, tool.meta)
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions test/mcp/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,14 @@ def test_tools_sends_request_to_transport_and_returns_tools_array
"tools" => [
{
"name" => "tool1",
"title" => "First tool",
"description" => "tool1",
"inputSchema" => {},
"outputSchema" => { "type" => "object", "properties" => { "result" => { "type" => "string" } } },
"annotations" => { "readOnlyHint" => true, "title" => "Tool One" },
"icons" => [{ "src" => "https://example.com/tool1.png", "mimeType" => "image/png" }],
"execution" => { "taskSupport" => "optional" },
"_meta" => { "ui" => { "resourceUri" => "ui://shop/plans", "visibility" => ["model", "app"] } },
},
{ "name" => "tool2", "description" => "tool2", "inputSchema" => {} },
],
Expand All @@ -358,9 +362,18 @@ def test_tools_sends_request_to_transport_and_returns_tools_array
assert_equal("tool1", tools.first.name)
assert_equal({ "type" => "object", "properties" => { "result" => { "type" => "string" } } }, tools.first.output_schema)
assert_equal({ "readOnlyHint" => true, "title" => "Tool One" }, tools.first.annotations)
# Kept as sent: an MCP Apps host reads the tool's app from `_meta.ui`.
assert_equal("First tool", tools.first.title)
assert_equal([{ "src" => "https://example.com/tool1.png", "mimeType" => "image/png" }], tools.first.icons)
assert_equal({ "taskSupport" => "optional" }, tools.first.execution)
assert_equal({ "ui" => { "resourceUri" => "ui://shop/plans", "visibility" => ["model", "app"] } }, tools.first.meta)
assert_equal("tool2", tools.last.name)
assert_nil(tools.last.output_schema)
assert_nil(tools.last.annotations)
assert_nil(tools.last.title)
assert_nil(tools.last.icons)
assert_nil(tools.last.execution)
assert_nil(tools.last.meta)
end

def test_tools_returns_empty_array_when_no_tools
Expand Down
Loading