Skip to content

Commit c8847ac

Browse files
committed
Keep a tool's _meta, title, icons, and execution on the client
## Motivation and Context `MCP::Client#list_tools` built each `MCP::Client::Tool` from `name`, `description`, `inputSchema`, `outputSchema`, and `annotations`, and dropped the other members of the specification's `Tool` object: `title` and `_meta` (2025-06-18) and `icons` and `execution` (2025-11-25). Extensions put what a host needs under `_meta`: MCP Apps (SEP-1865) names a tool's app under `_meta.ui`, so a host on this client could not tell which tools have an app without a second raw `tools/list` of its own (#640). The TypeScript and Python clients keep all four on their `Tool` types, and this SDK's own `MCP::Tool` already serves `title`, `icons`, and `_meta`. `MCP::Client::Tool` now keeps the four as the server sent them, the way `annotations` is kept, behind `title`, `icons`, `execution`, and `meta` readers; `list_tools` passes them through, and a member a tool omits reads as nil. The SDK gives none of them a meaning of its own: `execution` carries the task-support hint of SEP-1686, which this SDK does not act on yet, and `_meta` keys belong to the extensions that define them. The client page notes that all four are untrusted input from the server, which the SDK does not act on. Closes #640. ## How Has This Been Tested? test/mcp/client/tool_test.rb and test/mcp/client_test.rb, which read the four members back from a `tools/list` answer carrying the MCP Apps `_meta.ui` shape and see nil when a tool omits them. At d518025 the readers raise `NoMethodError` and `MCP::Client::Tool.new` refuses the keywords. ## Breaking Changes None. The new keywords of `MCP::Client::Tool#initialize` default to nil.
1 parent d518025 commit c8847ac

5 files changed

Lines changed: 44 additions & 2 deletions

File tree

‎docs/_client/index.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ The client provides a wrapper class for tools returned by the server:
4646
- `MCP::Client::Tool` - Represents a single tool with its metadata
4747

4848
This class provides easy access to tool properties like name, description, input schema, and output schema.
49+
`title`, `annotations`, `icons`, `execution`, and `meta` (the tool's `_meta`) are kept as the server sent them,
50+
so what an extension puts under `_meta` is readable: an MCP Apps host finds a tool's app under `tool.meta&.dig("ui", "resourceUri")`.
51+
These values are untrusted input from the server: the SDK passes them through without acting on them,
52+
and a host that renders `icons` or loads what `_meta` names treats them as it would any server-supplied URL or content.

‎lib/mcp/client.rb‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,14 @@ def list_tools(cursor: nil, meta: nil, cancellation: nil)
315315

316316
Tool.new(
317317
name: tool["name"],
318+
title: tool["title"],
318319
description: tool["description"],
319320
input_schema: tool["inputSchema"],
320321
output_schema: tool["outputSchema"],
321322
annotations: tool["annotations"],
323+
icons: tool["icons"],
324+
execution: tool["execution"],
325+
meta: tool["_meta"],
322326
)
323327
end
324328

‎lib/mcp/client/tool.rb‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22

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

8-
def initialize(name:, description:, input_schema:, output_schema: nil, annotations: nil)
10+
def initialize(name:, description:, input_schema:, output_schema: nil, annotations: nil, title: nil, icons: nil, execution: nil, meta: nil)
911
@name = name
12+
@title = title
1013
@description = description
1114
@input_schema = input_schema
1215
@output_schema = output_schema
1316
@annotations = annotations
17+
@icons = icons
18+
@execution = execution
19+
@meta = meta
1420
end
1521
end
1622
end

‎test/mcp/client/tool_test.rb‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ def test_annotations_returns_nil_when_not_provided
3737
assert_nil(@tool.annotations)
3838
end
3939

40+
def test_title_icons_execution_and_meta_return_nil_when_not_provided
41+
assert_nil(@tool.title)
42+
assert_nil(@tool.icons)
43+
assert_nil(@tool.execution)
44+
assert_nil(@tool.meta)
45+
end
46+
4047
def test_annotations_returns_annotations_when_provided
4148
tool_with_annotations = Tool.new(
4249
name: "test_tool_with_annotations",
@@ -68,17 +75,25 @@ def test_output_schema_returns_output_schema_when_provided
6875
def test_initialization_with_all_parameters
6976
tool = Tool.new(
7077
name: "full_tool",
78+
title: "Full Tool",
7179
description: "A tool with all parameters",
7280
input_schema: { "type" => "object" },
7381
output_schema: { "type" => "object", "properties" => { "status" => { "type" => "boolean" } } },
7482
annotations: { "readOnlyHint" => true },
83+
icons: [{ "src" => "https://example.com/tool.png", "mimeType" => "image/png" }],
84+
execution: { "taskSupport" => "optional" },
85+
meta: { "ui" => { "resourceUri" => "ui://shop/plans" } },
7586
)
7687

7788
assert_equal("full_tool", tool.name)
89+
assert_equal("Full Tool", tool.title)
7890
assert_equal("A tool with all parameters", tool.description)
7991
assert_equal({ "type" => "object" }, tool.input_schema)
8092
assert_equal({ "type" => "object", "properties" => { "status" => { "type" => "boolean" } } }, tool.output_schema)
8193
assert_equal({ "readOnlyHint" => true }, tool.annotations)
94+
assert_equal([{ "src" => "https://example.com/tool.png", "mimeType" => "image/png" }], tool.icons)
95+
assert_equal({ "taskSupport" => "optional" }, tool.execution)
96+
assert_equal({ "ui" => { "resourceUri" => "ui://shop/plans" } }, tool.meta)
8297
end
8398
end
8499
end

‎test/mcp/client_test.rb‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,14 @@ def test_tools_sends_request_to_transport_and_returns_tools_array
336336
"tools" => [
337337
{
338338
"name" => "tool1",
339+
"title" => "First tool",
339340
"description" => "tool1",
340341
"inputSchema" => {},
341342
"outputSchema" => { "type" => "object", "properties" => { "result" => { "type" => "string" } } },
342343
"annotations" => { "readOnlyHint" => true, "title" => "Tool One" },
344+
"icons" => [{ "src" => "https://example.com/tool1.png", "mimeType" => "image/png" }],
345+
"execution" => { "taskSupport" => "optional" },
346+
"_meta" => { "ui" => { "resourceUri" => "ui://shop/plans", "visibility" => ["model", "app"] } },
343347
},
344348
{ "name" => "tool2", "description" => "tool2", "inputSchema" => {} },
345349
],
@@ -358,9 +362,18 @@ def test_tools_sends_request_to_transport_and_returns_tools_array
358362
assert_equal("tool1", tools.first.name)
359363
assert_equal({ "type" => "object", "properties" => { "result" => { "type" => "string" } } }, tools.first.output_schema)
360364
assert_equal({ "readOnlyHint" => true, "title" => "Tool One" }, tools.first.annotations)
365+
# Kept as sent: an MCP Apps host reads the tool's app from `_meta.ui`.
366+
assert_equal("First tool", tools.first.title)
367+
assert_equal([{ "src" => "https://example.com/tool1.png", "mimeType" => "image/png" }], tools.first.icons)
368+
assert_equal({ "taskSupport" => "optional" }, tools.first.execution)
369+
assert_equal({ "ui" => { "resourceUri" => "ui://shop/plans", "visibility" => ["model", "app"] } }, tools.first.meta)
361370
assert_equal("tool2", tools.last.name)
362371
assert_nil(tools.last.output_schema)
363372
assert_nil(tools.last.annotations)
373+
assert_nil(tools.last.title)
374+
assert_nil(tools.last.icons)
375+
assert_nil(tools.last.execution)
376+
assert_nil(tools.last.meta)
364377
end
365378

366379
def test_tools_returns_empty_array_when_no_tools

0 commit comments

Comments
 (0)