anthropic 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +1 -1
  4. data/lib/anthropic/credentials/workload_identity.rb +1 -1
  5. data/lib/anthropic/helpers/messages.rb +13 -0
  6. data/lib/anthropic/helpers/tools/base_tool.rb +18 -0
  7. data/lib/anthropic/helpers/tools/mcp.rb +485 -0
  8. data/lib/anthropic/internal/transport/base_client.rb +2 -0
  9. data/lib/anthropic/mcp.rb +5 -0
  10. data/lib/anthropic/models/anthropic_beta.rb +3 -0
  11. data/lib/anthropic/models/beta/beta_cache_miss_messages_changed.rb +31 -0
  12. data/lib/anthropic/models/beta/beta_cache_miss_model_changed.rb +31 -0
  13. data/lib/anthropic/models/beta/beta_cache_miss_previous_message_not_found.rb +19 -0
  14. data/lib/anthropic/models/beta/beta_cache_miss_system_changed.rb +31 -0
  15. data/lib/anthropic/models/beta/beta_cache_miss_tools_changed.rb +31 -0
  16. data/lib/anthropic/models/beta/beta_cache_miss_unavailable.rb +19 -0
  17. data/lib/anthropic/models/beta/beta_diagnostics.rb +60 -0
  18. data/lib/anthropic/models/beta/beta_diagnostics_param.rb +30 -0
  19. data/lib/anthropic/models/beta/beta_managed_agents_outcome_evaluation_resource.rb +4 -4
  20. data/lib/anthropic/models/beta/beta_message.rb +10 -1
  21. data/lib/anthropic/models/beta/message_create_params.rb +10 -1
  22. data/lib/anthropic/models/beta/messages/batch_create_params.rb +10 -1
  23. data/lib/anthropic/models/beta/sessions/beta_managed_agents_agent_mcp_tool_result_event.rb +6 -3
  24. data/lib/anthropic/models/beta/sessions/beta_managed_agents_agent_tool_result_event.rb +6 -3
  25. data/lib/anthropic/models/beta/sessions/beta_managed_agents_search_result_block.rb +64 -0
  26. data/lib/anthropic/models/beta/sessions/beta_managed_agents_search_result_citations.rb +22 -0
  27. data/lib/anthropic/models/beta/sessions/beta_managed_agents_search_result_content.rb +39 -0
  28. data/lib/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event.rb +6 -3
  29. data/lib/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event_params.rb +6 -3
  30. data/lib/anthropic/resources/beta/messages.rb +7 -2
  31. data/lib/anthropic/resources/messages.rb +2 -2
  32. data/lib/anthropic/version.rb +1 -1
  33. data/lib/anthropic.rb +13 -0
  34. data/rbi/anthropic/helpers/aws/client.rbi +3 -2
  35. data/rbi/anthropic/models/anthropic_beta.rbi +5 -0
  36. data/rbi/anthropic/models/beta/beta_cache_miss_messages_changed.rbi +46 -0
  37. data/rbi/anthropic/models/beta/beta_cache_miss_model_changed.rbi +46 -0
  38. data/rbi/anthropic/models/beta/beta_cache_miss_previous_message_not_found.rbi +31 -0
  39. data/rbi/anthropic/models/beta/beta_cache_miss_system_changed.rbi +46 -0
  40. data/rbi/anthropic/models/beta/beta_cache_miss_tools_changed.rbi +46 -0
  41. data/rbi/anthropic/models/beta/beta_cache_miss_unavailable.rbi +30 -0
  42. data/rbi/anthropic/models/beta/beta_diagnostics.rbi +101 -0
  43. data/rbi/anthropic/models/beta/beta_diagnostics_param.rbi +48 -0
  44. data/rbi/anthropic/models/beta/beta_managed_agents_outcome_evaluation_resource.rbi +6 -6
  45. data/rbi/anthropic/models/beta/beta_message.rbi +17 -0
  46. data/rbi/anthropic/models/beta/message_create_params.rbi +19 -0
  47. data/rbi/anthropic/models/beta/messages/batch_create_params.rbi +20 -0
  48. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_agent_mcp_tool_result_event.rbi +6 -3
  49. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_agent_tool_result_event.rbi +6 -3
  50. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_search_result_block.rbi +136 -0
  51. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_search_result_citations.rbi +35 -0
  52. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_search_result_content.rbi +86 -0
  53. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event.rbi +6 -3
  54. data/rbi/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event_params.rbi +10 -5
  55. data/rbi/anthropic/resources/beta/messages.rbi +10 -0
  56. data/sig/anthropic/models/anthropic_beta.rbs +2 -0
  57. data/sig/anthropic/models/beta/beta_cache_miss_messages_changed.rbs +26 -0
  58. data/sig/anthropic/models/beta/beta_cache_miss_model_changed.rbs +26 -0
  59. data/sig/anthropic/models/beta/beta_cache_miss_previous_message_not_found.rbs +18 -0
  60. data/sig/anthropic/models/beta/beta_cache_miss_system_changed.rbs +26 -0
  61. data/sig/anthropic/models/beta/beta_cache_miss_tools_changed.rbs +26 -0
  62. data/sig/anthropic/models/beta/beta_cache_miss_unavailable.rbs +17 -0
  63. data/sig/anthropic/models/beta/beta_diagnostics.rbs +38 -0
  64. data/sig/anthropic/models/beta/beta_diagnostics_param.rbs +17 -0
  65. data/sig/anthropic/models/beta/beta_message.rbs +5 -0
  66. data/sig/anthropic/models/beta/message_create_params.rbs +5 -0
  67. data/sig/anthropic/models/beta/messages/batch_create_params.rbs +5 -0
  68. data/sig/anthropic/models/beta/sessions/beta_managed_agents_agent_mcp_tool_result_event.rbs +1 -0
  69. data/sig/anthropic/models/beta/sessions/beta_managed_agents_agent_tool_result_event.rbs +1 -0
  70. data/sig/anthropic/models/beta/sessions/beta_managed_agents_search_result_block.rbs +54 -0
  71. data/sig/anthropic/models/beta/sessions/beta_managed_agents_search_result_citations.rbs +17 -0
  72. data/sig/anthropic/models/beta/sessions/beta_managed_agents_search_result_content.rbs +39 -0
  73. data/sig/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event.rbs +1 -0
  74. data/sig/anthropic/models/beta/sessions/beta_managed_agents_user_custom_tool_result_event_params.rbs +1 -0
  75. data/sig/anthropic/resources/beta/messages.rbs +2 -0
  76. metadata +37 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b34378dd9395b126e18915a8ee15526d27d7e7002475e401d8c81bae601289fb
4
- data.tar.gz: ed103f7d82d22f04cc7a0332a4a24648beaafcbafbf4e16fa19717ce59c9817c
3
+ metadata.gz: fc1a7f57c129c15f70a549ddb40df06a00fab53532406c86722ec253de5247cc
4
+ data.tar.gz: e70e7a9bcd6e5fbc5f9bc904a7475cde990e7e9f393d232c2f2e4e53ec74cc62
5
5
  SHA512:
6
- metadata.gz: 6740871a358a65e3948a1381ba601ad342f4f1418c19af0e9c6badf1c91e7d4aec51650f5d202a3f31d44b39f4c951a85875d4208227281ceb0d7da0b7a9c0d8
7
- data.tar.gz: b596807165dbd16f19fe9902451b0e6e9b346030265214d1c5eece6e1d4eb8ee7c3fd9cf650d06ca7695eac94103b4669f8b130970b0fe5ca61022ea4ba0630f
6
+ metadata.gz: 76e09bc1ab37e2aed7659956e5cfa1c0511e5947940e76c1c465b5098c6bb50526d8b918f4d4c7a677a7eabfb71e557118e02dd3b55cf639c3a6958433706627
7
+ data.tar.gz: bcf9f66aae233ecb1dde11d4468cfc557529688a0490bde5d01036a1c87b480fc2a2814e487f34ff27640ef089f3ccbb7a3395bc0757d3d99fac5b0245479cbd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.41.0 (2026-05-13)
4
+
5
+ Full Changelog: [v1.40.0...v1.41.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.40.0...v1.41.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** Add BetaManagedAgentsSearchResultBlock types ([1432542](https://github.com/anthropics/anthropic-sdk-ruby/commit/1432542b303269f23d35e5b6891518a32b5fa934))
10
+ * **api:** Add support for cache diagnostics beta ([7ada493](https://github.com/anthropics/anthropic-sdk-ruby/commit/7ada4935e83b955b2f0b19e2940bc7dc3d336bc1))
11
+ * **mcp:** add mcp helpers ([#934](https://github.com/anthropics/anthropic-sdk-ruby/issues/934)) ([de5b608](https://github.com/anthropics/anthropic-sdk-ruby/commit/de5b60875486a62534fb95ca13153819c601af92))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * avoid gzip buffering during streaming on non-beta messages ([#936](https://github.com/anthropics/anthropic-sdk-ruby/issues/936)) ([8b7f779](https://github.com/anthropics/anthropic-sdk-ruby/commit/8b7f779bd9bdfc7693de679832531c32c215909a))
17
+ * **client:** elide content type header on requests without body ([c58745a](https://github.com/anthropics/anthropic-sdk-ruby/commit/c58745aea5725531ba89aac2d64b3914232833c2))
18
+ * handle syntax updates for ruby 4's PRISM parser ([#189](https://github.com/anthropics/anthropic-sdk-ruby/issues/189)) ([772dd65](https://github.com/anthropics/anthropic-sdk-ruby/commit/772dd65733bfd2869f846b6a9734776ed87ee7d6))
19
+
20
+
21
+ ### Chores
22
+
23
+ * **api:** spec updates ([0e85c3f](https://github.com/anthropics/anthropic-sdk-ruby/commit/0e85c3f8064dd1951ef00c06ac9a709f7067eb70))
24
+
3
25
  ## 1.40.0 (2026-05-11)
4
26
 
5
27
  Full Changelog: [v1.39.0...v1.40.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.39.0...v1.40.0)
data/README.md CHANGED
@@ -15,7 +15,7 @@ Add to your application's Gemfile:
15
15
  <!-- x-release-please-start-version -->
16
16
 
17
17
  ```ruby
18
- gem "anthropic", "~> 1.40.0"
18
+ gem "anthropic", "~> 1.41.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -158,7 +158,7 @@ module Anthropic
158
158
  data = JSON.parse(response.body, symbolize_names: true)
159
159
  token, expires_in =
160
160
  case data
161
- in {access_token: String => token, expires_in: Integer | String => expires_in}
161
+ in {access_token: String => token, expires_in: (Integer | String) => expires_in}
162
162
  [token, expires_in.to_i]
163
163
  in {access_token: String => token}
164
164
  [token, 3600]
@@ -35,8 +35,20 @@ module Anthropic
35
35
 
36
36
  case data
37
37
  in {tools: Array => tool_array}
38
+ # rubocop:disable Metrics/BlockLength
38
39
  mapped = tool_array.map do |tool|
39
40
  case tool
41
+ # BaseTool whose class declares an explicit `tool_name` (e.g. MCP-built tools):
42
+ in Anthropic::Helpers::Tools::BaseTool if tool.class.tool_name
43
+ name = tool.class.tool_name
44
+ description = tool.class.doc_string
45
+ tools.store(name, tool)
46
+ input_schema = Anthropic::Helpers::InputSchema::JsonSchemaConverter.to_json_schema(tool)
47
+ extras = tool.class.tool_extra_props || {}
48
+ result = {name:, input_schema:, **extras}
49
+ result[:description] = description if description
50
+ result[:strict] = strict if strict
51
+ result
40
52
  # Direct tool class:
41
53
  in Anthropic::Helpers::InputSchema::JsonSchemaConverter
42
54
  classname = tool.is_a?(Anthropic::Helpers::Tools::BaseTool) ? tool.class.name : tool.name
@@ -68,6 +80,7 @@ module Anthropic
68
80
  tool
69
81
  end
70
82
  end
83
+ # rubocop:enable Metrics/BlockLength
71
84
  tool_array.replace(mapped)
72
85
  # GA: output_config with BaseModel class as format
73
86
  in {output_config: {format: Anthropic::Helpers::InputSchema::JsonSchemaConverter => model} => output_config}
@@ -16,6 +16,24 @@ module Anthropic
16
16
  # @return [String]
17
17
  attr_reader :doc_string
18
18
 
19
+ # @api private
20
+ #
21
+ # When set, the runner uses this literal string as the API tool name instead of
22
+ # snake-casing the class name. Used by helpers (e.g. MCP) that build tools
23
+ # dynamically from an external definition.
24
+ #
25
+ # @return [String, nil]
26
+ attr_accessor :tool_name
27
+
28
+ # @api private
29
+ #
30
+ # Extra tool-definition properties merged into the API payload (e.g.
31
+ # `cache_control`, `defer_loading`, `allowed_callers`, `eager_input_streaming`,
32
+ # `input_examples`, `strict`).
33
+ #
34
+ # @return [Hash{Symbol=>Object}, nil]
35
+ attr_accessor :tool_extra_props
36
+
19
37
  # @api public
20
38
  #
21
39
  # @param description [String]
@@ -0,0 +1,485 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Helpers
5
+ module Tools
6
+ # Helpers for integrating Model Context Protocol (MCP) servers with the
7
+ # Anthropic SDK.
8
+ #
9
+ # These helpers convert types returned by the `mcp` gem into the shapes
10
+ # the Beta Messages API accepts, so you don't have to write glue code
11
+ # yourself.
12
+ #
13
+ # The `mcp` gem is an optional dependency; install it separately:
14
+ #
15
+ # gem install mcp
16
+ #
17
+ # @example Convert MCP tools and run them through `tool_runner`
18
+ # require "mcp"
19
+ # require "anthropic"
20
+ #
21
+ # transport = MCP::Client::HTTP.new(url: "https://example.com/mcp")
22
+ # mcp_client = MCP::Client.new(transport: transport)
23
+ # anthropic = Anthropic::Client.new
24
+ #
25
+ # runner = anthropic.beta.messages.tool_runner(
26
+ # model: "claude-sonnet-4-5",
27
+ # max_tokens: 1024,
28
+ # messages: [{role: "user", content: "Use the available tools"}],
29
+ # tools: Anthropic::Mcp.tools(mcp_client.tools, mcp_client)
30
+ # )
31
+ # runner.run_until_finished
32
+ module Mcp
33
+ SUPPORTED_IMAGE_TYPES = Set.new(%w[image/jpeg image/png image/gif image/webp]).freeze
34
+
35
+ # Raised when an MCP value cannot be converted to a format supported by
36
+ # the Claude API.
37
+ class UnsupportedMCPValueError < Anthropic::Errors::Error; end
38
+
39
+ # @api private
40
+ #
41
+ # Runnable tool backing {Anthropic::Mcp.tool}. Each call to {build} produces
42
+ # a fresh anonymous subclass — that subclass owns a unique inner "parsed
43
+ # input" class so the runner can dispatch tool calls via
44
+ # `class.model === tool_use.parsed`, the same path used by hand-written
45
+ # {Anthropic::BaseTool} subclasses.
46
+ class Tool < Anthropic::Helpers::Tools::BaseTool
47
+ class << self
48
+ attr_accessor :mcp_input_schema, :mcp_client
49
+ end
50
+
51
+ def self.build(
52
+ mcp_tool:,
53
+ mcp_client:,
54
+ cache_control: nil,
55
+ defer_loading: nil,
56
+ allowed_callers: nil,
57
+ eager_input_streaming: nil,
58
+ input_examples: nil,
59
+ strict: nil
60
+ )
61
+ api_name, description, raw_schema = Mcp.send(:extract_tool_fields, mcp_tool)
62
+ raise ArgumentError, "MCP tool is missing a `name`" if api_name.to_s.empty?
63
+
64
+ extras = {
65
+ cache_control: cache_control,
66
+ defer_loading: defer_loading,
67
+ allowed_callers: allowed_callers,
68
+ eager_input_streaming: eager_input_streaming,
69
+ input_examples: input_examples,
70
+ strict: strict
71
+ }.compact
72
+
73
+ input_class = Class.new(Hash)
74
+
75
+ klass = Class.new(self)
76
+ klass.description(description) if description
77
+ klass.tool_name = api_name
78
+ klass.tool_extra_props = extras
79
+ klass.mcp_input_schema = Mcp.send(:normalize_schema, raw_schema)
80
+ klass.mcp_client = mcp_client
81
+ klass.instance_variable_set(:@model, input_class)
82
+ klass.new
83
+ end
84
+
85
+ # Return a deep copy of the raw schema; `SupportedSchemas.transform_schema!`
86
+ # mutates the hash it receives.
87
+ def to_json_schema_inner(state:)
88
+ _ = state
89
+ Marshal.load(Marshal.dump(self.class.mcp_input_schema))
90
+ end
91
+
92
+ def to_json_schema
93
+ Anthropic::Helpers::InputSchema::JsonSchemaConverter.to_json_schema(self)
94
+ end
95
+
96
+ # MCP arguments are opaque JSON objects — pass through unchanged. The
97
+ # returned object is an instance of this subclass's per-tool `model`,
98
+ # which is both a Hash (for downstream serialization) and uniquely
99
+ # typed so the runner can identify which MCP tool the parsed input
100
+ # belongs to.
101
+ def coerce(value, state:)
102
+ state.fetch(:exactness)[:yes] += 1
103
+ wrapper = self.class.model.new
104
+ input = if value.is_a?(Hash)
105
+ value
106
+ else
107
+ (value.respond_to?(:to_h) ? value.to_h : {})
108
+ end
109
+ input.each { |k, v| wrapper[k] = v }
110
+ wrapper
111
+ end
112
+
113
+ def call(parsed)
114
+ args = parsed.is_a?(Hash) ? parsed.to_h : parsed
115
+ response = self.class.mcp_client.call_tool(name: self.class.tool_name, arguments: args)
116
+ Mcp.send(:convert_tool_result, response)
117
+ end
118
+ end
119
+
120
+ class << self
121
+ # Convert an MCP tool definition into a runnable tool for `tool_runner`.
122
+ #
123
+ # @param mcp_tool [MCP::Client::Tool, Hash] An MCP tool, typically from
124
+ # `mcp_client.list_tools` / `mcp_client.tools`. May be a typed
125
+ # `MCP::Client::Tool` or a hash with `:name`, `:description`,
126
+ # `:input_schema` (or `:inputSchema`).
127
+ # @param mcp_client [#call_tool] The MCP client used to invoke the tool.
128
+ # @param cache_control [Hash, nil] Prompt-caching control passed through
129
+ # to the tool definition.
130
+ # @param defer_loading [Boolean, nil] If true, the tool is excluded
131
+ # from the initial system prompt.
132
+ # @param allowed_callers [Array<Symbol, String>, nil] Restricts which
133
+ # callers may invoke the tool.
134
+ # @param eager_input_streaming [Boolean, nil] Enables eager input
135
+ # streaming for the tool.
136
+ # @param input_examples [Array<Hash>, nil] Example inputs for the tool.
137
+ # @param strict [Boolean, nil] When true, guarantees schema validation
138
+ # on tool names and inputs.
139
+ # @return [Anthropic::Helpers::Tools::Mcp::Tool]
140
+ def tool(
141
+ mcp_tool,
142
+ mcp_client,
143
+ cache_control: nil,
144
+ defer_loading: nil,
145
+ allowed_callers: nil,
146
+ eager_input_streaming: nil,
147
+ input_examples: nil,
148
+ strict: nil
149
+ )
150
+ require_mcp!
151
+ Tool.build(
152
+ mcp_tool: mcp_tool,
153
+ mcp_client: mcp_client,
154
+ cache_control: cache_control,
155
+ defer_loading: defer_loading,
156
+ allowed_callers: allowed_callers,
157
+ eager_input_streaming: eager_input_streaming,
158
+ input_examples: input_examples,
159
+ strict: strict
160
+ )
161
+ end
162
+
163
+ # Convert a list of MCP tools into runnable tools.
164
+ #
165
+ # @param mcp_tools [Array<MCP::Client::Tool, Hash>]
166
+ # @param mcp_client [#call_tool]
167
+ # @return [Array<Anthropic::Helpers::Tools::Mcp::Tool>]
168
+ def tools(mcp_tools, mcp_client, **opts)
169
+ mcp_tools.map { tool(_1, mcp_client, **opts) }
170
+ end
171
+
172
+ # Convert an MCP prompt message into a Beta message parameter.
173
+ #
174
+ # @param mcp_message [MCP::Prompt::Message, Hash]
175
+ # @param cache_control [Hash, nil] Forwarded to the produced content block.
176
+ # @return [Hash]
177
+ def message(mcp_message, cache_control: nil)
178
+ require_mcp!
179
+ h = to_hash!(mcp_message, "MCP prompt message")
180
+ role = hkey(h, :role)
181
+ role = role.to_sym if role.respond_to?(:to_sym)
182
+ {
183
+ role: role,
184
+ content: [content(hkey(h, :content), cache_control: cache_control)]
185
+ }
186
+ end
187
+
188
+ # Convert a single MCP content item into a Beta content block.
189
+ #
190
+ # Handles text, image, and embedded resource content types. Raises
191
+ # {UnsupportedMCPValueError} for audio or resource_link types.
192
+ #
193
+ # @param mcp_content [MCP::Content::Text, MCP::Content::Image,
194
+ # MCP::Content::EmbeddedResource, Hash]
195
+ # @param cache_control [Hash, nil]
196
+ # @return [Hash]
197
+ def content(mcp_content, cache_control: nil)
198
+ require_mcp!
199
+ h = to_hash!(mcp_content, "MCP content")
200
+ block = convert_content(h)
201
+ block[:cache_control] = cache_control if cache_control
202
+ block
203
+ end
204
+
205
+ # Convert MCP resource read results into Beta content blocks — one
206
+ # per item in `contents`. Raises {UnsupportedMCPValueError} on any
207
+ # item whose MIME type is unsupported.
208
+ #
209
+ # @param result [Hash, Array, #contents] The result from
210
+ # `mcp_client.read_resource(uri: ...)`. The `mcp` gem returns just
211
+ # the contents array — both that and a `{contents: [...]}` hash are
212
+ # accepted.
213
+ # @param cache_control [Hash, nil] Forwarded to each produced block.
214
+ # @return [Array<Hash>]
215
+ def resource_to_contents(result, cache_control: nil)
216
+ require_mcp!
217
+ contents = extract_resource_contents(result)
218
+ if contents.empty?
219
+ raise UnsupportedMCPValueError,
220
+ "Resource contents array must contain at least one item"
221
+ end
222
+
223
+ contents.map do |item|
224
+ block = resource_contents_to_block(to_hash!(item, "resource"))
225
+ block[:cache_control] = cache_control if cache_control
226
+ block
227
+ end
228
+ end
229
+
230
+ # Convert MCP resource read results into {Anthropic::FilePart}
231
+ # instances suitable for `client.beta.files.upload(file: ...)`. No
232
+ # MIME filtering — every item in `contents` becomes a file.
233
+ #
234
+ # @param result [Hash, Array, #contents]
235
+ # @return [Array<Anthropic::FilePart>]
236
+ def resource_to_files(result)
237
+ require_mcp!
238
+ contents = extract_resource_contents(result)
239
+ if contents.empty?
240
+ raise UnsupportedMCPValueError,
241
+ "Resource contents array must contain at least one item"
242
+ end
243
+
244
+ contents.map do |item|
245
+ resource = to_hash!(item, "resource")
246
+ Anthropic::FilePart.new(
247
+ StringIO.new(resource_bytes(resource)),
248
+ filename: filename_from_uri(hkey(resource, :uri)),
249
+ content_type: hkey(resource, :mimeType)
250
+ )
251
+ end
252
+ end
253
+
254
+ # @api private
255
+ # Called by {Tool#call}; converts a JSON-RPC `tools/call` response
256
+ # into the value the tool runner expects.
257
+ def convert_tool_result(response)
258
+ result = nested_result(response)
259
+ is_error = hkey(result, :isError)
260
+ content_items = hkey(result, :content) || []
261
+ structured = hkey(result, :structuredContent)
262
+
263
+ if is_error
264
+ blocks = content_items.map { content(_1) }
265
+ raise Anthropic::Errors::Error, render_error_blocks(blocks)
266
+ end
267
+
268
+ return JSON.generate(structured) if content_items.empty? && structured
269
+
270
+ content_items.map { content(_1) }
271
+ end
272
+
273
+ # -- conversion internals -------------------------------------------
274
+
275
+ private def convert_content(h)
276
+ type = hkey(h, :type).to_s
277
+ case type
278
+ when "text"
279
+ text = hkey(h, :text)
280
+ {type: :text, text: text.to_s}
281
+ when "image"
282
+ data = hkey(h, :data)
283
+ mime = hkey(h, :mimeType)
284
+ unless supported_image_mime?(mime)
285
+ raise UnsupportedMCPValueError, "Unsupported image MIME type: #{mime}"
286
+ end
287
+ {type: :image, source: {type: :base64, data: data, media_type: mime}}
288
+ when "resource"
289
+ resource = hkey(h, :resource)
290
+ resource_contents_to_block(to_hash!(resource, "embedded resource"))
291
+ else
292
+ # Covers "audio", "resource_link", and any unrecognized type.
293
+ raise UnsupportedMCPValueError, "Unsupported MCP content type: #{type}"
294
+ end
295
+ end
296
+
297
+ private def resource_contents_to_block(resource)
298
+ mime = hkey(resource, :mimeType)
299
+ uri = hkey(resource, :uri)
300
+ text = hkey(resource, :text)
301
+ # `mcp/sdk` schema uses `blob`; the Ruby `mcp` gem's BlobContents#to_h also uses `blob`.
302
+ blob = hkey(resource, :blob)
303
+
304
+ if mime && supported_image_mime?(mime)
305
+ if blob.nil?
306
+ raise UnsupportedMCPValueError,
307
+ "Image resource must have blob data, not text. URI: #{uri}"
308
+ end
309
+ return {type: :image, source: {type: :base64, data: blob, media_type: mime}}
310
+ end
311
+
312
+ if mime == "application/pdf"
313
+ if blob.nil?
314
+ raise UnsupportedMCPValueError,
315
+ "PDF resource must have blob data, not text. URI: #{uri}"
316
+ end
317
+ return {type: :document, source: {type: :base64, data: blob, media_type: "application/pdf"}}
318
+ end
319
+
320
+ if mime.nil? || mime.start_with?("text/")
321
+ data = if !text.nil?
322
+ text.to_s
323
+ elsif !blob.nil?
324
+ Base64.decode64(blob.to_s).force_encoding(Encoding::UTF_8)
325
+ else
326
+ ""
327
+ end
328
+ return {type: :document, source: {type: :text, data: data, media_type: "text/plain"}}
329
+ end
330
+
331
+ raise UnsupportedMCPValueError, "Unsupported MIME type \"#{mime}\" for resource: #{uri}"
332
+ end
333
+
334
+ private def extract_resource_contents(result)
335
+ case result
336
+ when Array
337
+ result
338
+ when Hash
339
+ result[:contents] || result["contents"] || []
340
+ else
341
+ if result.respond_to?(:contents)
342
+ result.contents
343
+ elsif result.respond_to?(:to_h)
344
+ h = result.to_h
345
+ h[:contents] || h["contents"] || []
346
+ else
347
+ []
348
+ end
349
+ end
350
+ end
351
+
352
+ private def resource_bytes(resource)
353
+ text = hkey(resource, :text)
354
+ blob = hkey(resource, :blob)
355
+
356
+ return Base64.decode64(blob.to_s) unless blob.nil?
357
+ return text.to_s.dup.force_encoding(Encoding::UTF_8) unless text.nil?
358
+
359
+ "".dup
360
+ end
361
+
362
+ private def filename_from_uri(uri)
363
+ return "file" if uri.nil? || uri.to_s.empty?
364
+
365
+ path =
366
+ begin
367
+ URI.parse(uri.to_s).path
368
+ rescue URI::InvalidURIError
369
+ uri.to_s
370
+ end
371
+ base = path.to_s.split("/").last
372
+ base.nil? || base.empty? ? "file" : base
373
+ end
374
+
375
+ private def render_error_blocks(blocks)
376
+ parts = blocks.map do |b|
377
+ if b[:type] == :text
378
+ b[:text].to_s
379
+ else
380
+ JSON.generate(b)
381
+ end
382
+ end
383
+ joined = parts.reject(&:empty?).join("\n")
384
+ joined.empty? ? "MCP tool reported an error" : joined
385
+ end
386
+
387
+ private def nested_result(response)
388
+ return {} if response.nil?
389
+
390
+ r = response
391
+ r = r["result"] || r[:result] || r if r.is_a?(Hash) && (r.key?("result") || r.key?(:result))
392
+ r.is_a?(Hash) ? r : {}
393
+ end
394
+
395
+ # -- input shape helpers --------------------------------------------
396
+
397
+ private def extract_tool_fields(mcp_tool)
398
+ if mcp_tool.is_a?(Hash)
399
+ [
400
+ hkey(mcp_tool, :name),
401
+ hkey(mcp_tool, :description),
402
+ hkey(mcp_tool, :input_schema) || hkey(mcp_tool, :inputSchema)
403
+ ]
404
+ else
405
+ name = mcp_tool.respond_to?(:name) ? mcp_tool.name : nil
406
+ description = mcp_tool.respond_to?(:description) ? mcp_tool.description : nil
407
+ schema =
408
+ if mcp_tool.respond_to?(:input_schema)
409
+ mcp_tool.input_schema
410
+ elsif mcp_tool.respond_to?(:inputSchema)
411
+ mcp_tool.inputSchema
412
+ end
413
+ [name, description, schema]
414
+ end
415
+ end
416
+
417
+ # `mcp/sdk` requires `properties` and `required` to be present (even
418
+ # if null) on the API side. Normalize the gem's string-keyed
419
+ # JSON-Schema hashes into the symbol-keyed shape the SDK serializes.
420
+ private def normalize_schema(raw)
421
+ schema = raw.nil? ? {} : deep_symbolize_keys(raw)
422
+ schema[:type] ||= :object
423
+ schema[:properties] = nil unless schema.key?(:properties)
424
+ schema[:required] = nil unless schema.key?(:required)
425
+ schema
426
+ end
427
+
428
+ private def deep_symbolize_keys(obj)
429
+ case obj
430
+ in Hash
431
+ obj.each_with_object({}) do |(k, v), acc|
432
+ key = k.is_a?(String) ? k.to_sym : k
433
+ acc[key] = deep_symbolize_keys(v)
434
+ end
435
+ in Array
436
+ obj.map { deep_symbolize_keys(_1) }
437
+ else
438
+ obj
439
+ end
440
+ end
441
+
442
+ # Reads a value from a hash that may use either symbol or string keys.
443
+ # The Ruby `mcp` gem returns string-keyed hashes from transport layers
444
+ # but symbol-keyed hashes from typed objects' `to_h`.
445
+ private def hkey(h, key)
446
+ return nil unless h.is_a?(Hash)
447
+
448
+ sym = key.to_sym
449
+ str = key.to_s
450
+ return h[sym] if h.key?(sym)
451
+ return h[str] if h.key?(str)
452
+
453
+ nil
454
+ end
455
+
456
+ private def to_hash!(obj, label)
457
+ return obj if obj.is_a?(Hash)
458
+ return obj.to_h if obj.respond_to?(:to_h)
459
+
460
+ raise UnsupportedMCPValueError,
461
+ "Expected #{label} to be a Hash or to-hashable object, got #{obj.class}"
462
+ end
463
+
464
+ private def supported_image_mime?(mime)
465
+ return false unless mime.is_a?(String)
466
+
467
+ SUPPORTED_IMAGE_TYPES.include?(mime)
468
+ end
469
+
470
+ private def require_mcp!
471
+ return if defined?(::MCP)
472
+
473
+ begin
474
+ require("mcp")
475
+ rescue LoadError
476
+ raise LoadError,
477
+ "The `mcp` gem is required to use Anthropic's MCP helpers. " \
478
+ "Install it by adding `gem \"mcp\"` to your Gemfile (or running `gem install mcp`)."
479
+ end
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
@@ -310,6 +310,8 @@ module Anthropic
310
310
  Anthropic::Internal::Util.deep_merge(*[req[:body], opts[:extra_body]].compact)
311
311
  end
312
312
 
313
+ headers.delete("content-type") if body.nil?
314
+
313
315
  url = Anthropic::Internal::Util.join_parsed_uri(
314
316
  @base_url_components,
315
317
  {**req, path: path, query: query}
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ Mcp = Anthropic::Helpers::Tools::Mcp
5
+ end
@@ -55,6 +55,8 @@ module Anthropic
55
55
 
56
56
  variant const: -> { Anthropic::Models::AnthropicBeta::MANAGED_AGENTS_2026_04_01 }
57
57
 
58
+ variant const: -> { Anthropic::Models::AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07 }
59
+
58
60
  # @!method self.variants
59
61
  # @return [Array(String, Symbol)]
60
62
 
@@ -88,6 +90,7 @@ module Anthropic
88
90
  USER_PROFILES_2026_03_24 = :"user-profiles-2026-03-24"
89
91
  ADVISOR_TOOL_2026_03_01 = :"advisor-tool-2026-03-01"
90
92
  MANAGED_AGENTS_2026_04_01 = :"managed-agents-2026-04-01"
93
+ CACHE_DIAGNOSIS_2026_04_07 = :"cache-diagnosis-2026-04-07"
91
94
 
92
95
  # @!endgroup
93
96
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Models
5
+ module Beta
6
+ class BetaCacheMissMessagesChanged < Anthropic::Internal::Type::BaseModel
7
+ # @!attribute cache_missed_input_tokens
8
+ # Approximate number of input tokens that would have been read from cache had the
9
+ # prefix matched the previous request.
10
+ #
11
+ # @return [Integer]
12
+ required :cache_missed_input_tokens, Integer
13
+
14
+ # @!attribute type
15
+ #
16
+ # @return [Symbol, :messages_changed]
17
+ required :type, const: :messages_changed
18
+
19
+ # @!method initialize(cache_missed_input_tokens:, type: :messages_changed)
20
+ # Some parameter documentations has been truncated, see
21
+ # {Anthropic::Models::Beta::BetaCacheMissMessagesChanged} for more details.
22
+ #
23
+ # @param cache_missed_input_tokens [Integer] Approximate number of input tokens that would have been read from cache had the
24
+ #
25
+ # @param type [Symbol, :messages_changed]
26
+ end
27
+ end
28
+
29
+ BetaCacheMissMessagesChanged = Beta::BetaCacheMissMessagesChanged
30
+ end
31
+ end