claude-agent-sdk 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +432 -0
- data/lib/claude_agent_sdk/errors.rb +53 -0
- data/lib/claude_agent_sdk/message_parser.rb +110 -0
- data/lib/claude_agent_sdk/query.rb +442 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +165 -0
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +365 -0
- data/lib/claude_agent_sdk/transport.rb +44 -0
- data/lib/claude_agent_sdk/types.rb +358 -0
- data/lib/claude_agent_sdk/version.rb +5 -0
- data/lib/claude_agent_sdk.rb +256 -0
- metadata +126 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'async'
|
|
5
|
+
require 'async/queue'
|
|
6
|
+
require 'async/condition'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require_relative 'transport'
|
|
9
|
+
|
|
10
|
+
module ClaudeAgentSDK
|
|
11
|
+
# Handles bidirectional control protocol on top of Transport
|
|
12
|
+
#
|
|
13
|
+
# This class manages:
|
|
14
|
+
# - Control request/response routing
|
|
15
|
+
# - Hook callbacks
|
|
16
|
+
# - Tool permission callbacks
|
|
17
|
+
# - Message streaming
|
|
18
|
+
# - Initialization handshake
|
|
19
|
+
class Query
|
|
20
|
+
attr_reader :transport, :is_streaming_mode, :sdk_mcp_servers
|
|
21
|
+
|
|
22
|
+
def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil)
|
|
23
|
+
@transport = transport
|
|
24
|
+
@is_streaming_mode = is_streaming_mode
|
|
25
|
+
@can_use_tool = can_use_tool
|
|
26
|
+
@hooks = hooks || {}
|
|
27
|
+
@sdk_mcp_servers = sdk_mcp_servers || {}
|
|
28
|
+
|
|
29
|
+
# Control protocol state
|
|
30
|
+
@pending_control_responses = {}
|
|
31
|
+
@pending_control_results = {}
|
|
32
|
+
@hook_callbacks = {}
|
|
33
|
+
@next_callback_id = 0
|
|
34
|
+
@request_counter = 0
|
|
35
|
+
|
|
36
|
+
# Message stream
|
|
37
|
+
@message_queue = Async::Queue.new
|
|
38
|
+
@task = nil
|
|
39
|
+
@initialized = false
|
|
40
|
+
@closed = false
|
|
41
|
+
@initialization_result = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Initialize control protocol if in streaming mode
|
|
45
|
+
# @return [Hash, nil] Initialize response with supported commands, or nil if not streaming
|
|
46
|
+
def initialize_protocol
|
|
47
|
+
return nil unless @is_streaming_mode
|
|
48
|
+
|
|
49
|
+
# Build hooks configuration for initialization
|
|
50
|
+
hooks_config = {}
|
|
51
|
+
if @hooks && !@hooks.empty?
|
|
52
|
+
@hooks.each do |event, matchers|
|
|
53
|
+
next if matchers.nil? || matchers.empty?
|
|
54
|
+
|
|
55
|
+
hooks_config[event] = []
|
|
56
|
+
matchers.each do |matcher|
|
|
57
|
+
callback_ids = []
|
|
58
|
+
(matcher[:hooks] || []).each do |callback|
|
|
59
|
+
callback_id = "hook_#{@next_callback_id}"
|
|
60
|
+
@next_callback_id += 1
|
|
61
|
+
@hook_callbacks[callback_id] = callback
|
|
62
|
+
callback_ids << callback_id
|
|
63
|
+
end
|
|
64
|
+
hooks_config[event] << {
|
|
65
|
+
matcher: matcher[:matcher],
|
|
66
|
+
hookCallbackIds: callback_ids
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Send initialize request
|
|
73
|
+
request = {
|
|
74
|
+
subtype: 'initialize',
|
|
75
|
+
hooks: hooks_config.empty? ? nil : hooks_config
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
response = send_control_request(request)
|
|
79
|
+
@initialized = true
|
|
80
|
+
@initialization_result = response
|
|
81
|
+
response
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Start reading messages from transport
|
|
85
|
+
def start
|
|
86
|
+
return if @task
|
|
87
|
+
|
|
88
|
+
@task = Async do |task|
|
|
89
|
+
task.async { read_messages }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def read_messages
|
|
96
|
+
@transport.read_messages do |message|
|
|
97
|
+
break if @closed
|
|
98
|
+
|
|
99
|
+
msg_type = message[:type]
|
|
100
|
+
|
|
101
|
+
# Route control messages
|
|
102
|
+
case msg_type
|
|
103
|
+
when 'control_response'
|
|
104
|
+
handle_control_response(message)
|
|
105
|
+
when 'control_request'
|
|
106
|
+
Async { handle_control_request(message) }
|
|
107
|
+
when 'control_cancel_request'
|
|
108
|
+
# TODO: Implement cancellation support
|
|
109
|
+
next
|
|
110
|
+
else
|
|
111
|
+
# Regular SDK messages go to the queue
|
|
112
|
+
@message_queue.enqueue(message)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
# Put error in queue so iterators can handle it
|
|
117
|
+
@message_queue.enqueue({ type: 'error', error: e.message })
|
|
118
|
+
ensure
|
|
119
|
+
# Always signal end of stream
|
|
120
|
+
@message_queue.enqueue({ type: 'end' })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_control_response(message)
|
|
124
|
+
response = message[:response] || {}
|
|
125
|
+
request_id = response[:request_id]
|
|
126
|
+
return unless @pending_control_responses.key?(request_id)
|
|
127
|
+
|
|
128
|
+
if response[:subtype] == 'error'
|
|
129
|
+
@pending_control_results[request_id] = StandardError.new(response[:error] || 'Unknown error')
|
|
130
|
+
else
|
|
131
|
+
@pending_control_results[request_id] = response
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Signal that response is ready
|
|
135
|
+
@pending_control_responses[request_id].signal
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def handle_control_request(request)
|
|
139
|
+
request_id = request[:request_id]
|
|
140
|
+
request_data = request[:request]
|
|
141
|
+
subtype = request_data[:subtype]
|
|
142
|
+
|
|
143
|
+
response_data = {}
|
|
144
|
+
|
|
145
|
+
case subtype
|
|
146
|
+
when 'can_use_tool'
|
|
147
|
+
response_data = handle_permission_request(request_data)
|
|
148
|
+
when 'hook_callback'
|
|
149
|
+
response_data = handle_hook_callback(request_data)
|
|
150
|
+
when 'mcp_message'
|
|
151
|
+
response_data = handle_mcp_message(request_data)
|
|
152
|
+
else
|
|
153
|
+
raise "Unsupported control request subtype: #{subtype}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Send success response
|
|
157
|
+
success_response = {
|
|
158
|
+
type: 'control_response',
|
|
159
|
+
response: {
|
|
160
|
+
subtype: 'success',
|
|
161
|
+
request_id: request_id,
|
|
162
|
+
response: response_data
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
@transport.write(JSON.generate(success_response) + "\n")
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
# Send error response
|
|
168
|
+
error_response = {
|
|
169
|
+
type: 'control_response',
|
|
170
|
+
response: {
|
|
171
|
+
subtype: 'error',
|
|
172
|
+
request_id: request_id,
|
|
173
|
+
error: e.message
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
@transport.write(JSON.generate(error_response) + "\n")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def handle_permission_request(request_data)
|
|
180
|
+
raise 'canUseTool callback is not provided' unless @can_use_tool
|
|
181
|
+
|
|
182
|
+
original_input = request_data[:input]
|
|
183
|
+
|
|
184
|
+
context = ToolPermissionContext.new(
|
|
185
|
+
signal: nil,
|
|
186
|
+
suggestions: request_data[:permission_suggestions] || []
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
response = @can_use_tool.call(
|
|
190
|
+
request_data[:tool_name],
|
|
191
|
+
request_data[:input],
|
|
192
|
+
context
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Convert PermissionResult to expected format
|
|
196
|
+
case response
|
|
197
|
+
when PermissionResultAllow
|
|
198
|
+
result = {
|
|
199
|
+
behavior: 'allow',
|
|
200
|
+
updatedInput: response.updated_input || original_input
|
|
201
|
+
}
|
|
202
|
+
if response.updated_permissions
|
|
203
|
+
result[:updatedPermissions] = response.updated_permissions.map(&:to_h)
|
|
204
|
+
end
|
|
205
|
+
result
|
|
206
|
+
when PermissionResultDeny
|
|
207
|
+
result = { behavior: 'deny', message: response.message }
|
|
208
|
+
result[:interrupt] = response.interrupt if response.interrupt
|
|
209
|
+
result
|
|
210
|
+
else
|
|
211
|
+
raise "Tool permission callback must return PermissionResult, got #{response.class}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_hook_callback(request_data)
|
|
216
|
+
callback_id = request_data[:callback_id]
|
|
217
|
+
callback = @hook_callbacks[callback_id]
|
|
218
|
+
raise "No hook callback found for ID: #{callback_id}" unless callback
|
|
219
|
+
|
|
220
|
+
hook_output = callback.call(
|
|
221
|
+
request_data[:input],
|
|
222
|
+
request_data[:tool_use_id],
|
|
223
|
+
{ signal: nil }
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Convert Ruby-safe field names to CLI-expected names
|
|
227
|
+
convert_hook_output_for_cli(hook_output)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def handle_mcp_message(request_data)
|
|
231
|
+
server_name = request_data[:server_name]
|
|
232
|
+
mcp_message = request_data[:message]
|
|
233
|
+
|
|
234
|
+
raise 'Missing server_name or message for MCP request' unless server_name && mcp_message
|
|
235
|
+
|
|
236
|
+
mcp_response = handle_sdk_mcp_request(server_name, mcp_message)
|
|
237
|
+
{ mcp_response: mcp_response }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def convert_hook_output_for_cli(hook_output)
|
|
241
|
+
# Convert Ruby hash with symbol keys to CLI format
|
|
242
|
+
# Handle special keywords that might be Ruby-safe versions
|
|
243
|
+
converted = {}
|
|
244
|
+
hook_output.each do |key, value|
|
|
245
|
+
converted_key = case key
|
|
246
|
+
when :async_, 'async_' then 'async'
|
|
247
|
+
when :continue_, 'continue_' then 'continue'
|
|
248
|
+
else key.to_s.gsub('_', '')
|
|
249
|
+
end
|
|
250
|
+
converted[converted_key] = value
|
|
251
|
+
end
|
|
252
|
+
converted
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def send_control_request(request)
|
|
256
|
+
raise 'Control requests require streaming mode' unless @is_streaming_mode
|
|
257
|
+
|
|
258
|
+
# Generate unique request ID
|
|
259
|
+
@request_counter += 1
|
|
260
|
+
request_id = "req_#{@request_counter}_#{SecureRandom.hex(4)}"
|
|
261
|
+
|
|
262
|
+
# Create condition for response
|
|
263
|
+
condition = Async::Condition.new
|
|
264
|
+
@pending_control_responses[request_id] = condition
|
|
265
|
+
|
|
266
|
+
# Build and send request
|
|
267
|
+
control_request = {
|
|
268
|
+
type: 'control_request',
|
|
269
|
+
request_id: request_id,
|
|
270
|
+
request: request
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@transport.write(JSON.generate(control_request) + "\n")
|
|
274
|
+
|
|
275
|
+
# Wait for response with timeout
|
|
276
|
+
Async do |task|
|
|
277
|
+
task.with_timeout(60.0) do
|
|
278
|
+
condition.wait
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
result = @pending_control_results.delete(request_id)
|
|
282
|
+
@pending_control_responses.delete(request_id)
|
|
283
|
+
|
|
284
|
+
raise result if result.is_a?(Exception)
|
|
285
|
+
|
|
286
|
+
result[:response] || {}
|
|
287
|
+
end.wait
|
|
288
|
+
rescue Async::TimeoutError
|
|
289
|
+
@pending_control_responses.delete(request_id)
|
|
290
|
+
@pending_control_results.delete(request_id)
|
|
291
|
+
raise "Control request timeout: #{request[:subtype]}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def handle_sdk_mcp_request(server_name, message)
|
|
295
|
+
unless @sdk_mcp_servers.key?(server_name)
|
|
296
|
+
return {
|
|
297
|
+
jsonrpc: '2.0',
|
|
298
|
+
id: message[:id],
|
|
299
|
+
error: {
|
|
300
|
+
code: -32601,
|
|
301
|
+
message: "Server '#{server_name}' not found"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
server = @sdk_mcp_servers[server_name]
|
|
307
|
+
method = message[:method]
|
|
308
|
+
params = message[:params] || {}
|
|
309
|
+
|
|
310
|
+
case method
|
|
311
|
+
when 'initialize'
|
|
312
|
+
handle_mcp_initialize(server, message)
|
|
313
|
+
when 'tools/list'
|
|
314
|
+
handle_mcp_tools_list(server, message)
|
|
315
|
+
when 'tools/call'
|
|
316
|
+
handle_mcp_tools_call(server, message, params)
|
|
317
|
+
when 'notifications/initialized'
|
|
318
|
+
{ jsonrpc: '2.0', result: {} }
|
|
319
|
+
else
|
|
320
|
+
{
|
|
321
|
+
jsonrpc: '2.0',
|
|
322
|
+
id: message[:id],
|
|
323
|
+
error: { code: -32601, message: "Method '#{method}' not found" }
|
|
324
|
+
}
|
|
325
|
+
end
|
|
326
|
+
rescue StandardError => e
|
|
327
|
+
{
|
|
328
|
+
jsonrpc: '2.0',
|
|
329
|
+
id: message[:id],
|
|
330
|
+
error: { code: -32603, message: e.message }
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def handle_mcp_initialize(server, message)
|
|
335
|
+
{
|
|
336
|
+
jsonrpc: '2.0',
|
|
337
|
+
id: message[:id],
|
|
338
|
+
result: {
|
|
339
|
+
protocolVersion: '2024-11-05',
|
|
340
|
+
capabilities: { tools: {} },
|
|
341
|
+
serverInfo: {
|
|
342
|
+
name: server.name,
|
|
343
|
+
version: server.version || '1.0.0'
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def handle_mcp_tools_list(server, message)
|
|
350
|
+
# List tools from the SDK MCP server
|
|
351
|
+
tools_data = server.list_tools
|
|
352
|
+
{
|
|
353
|
+
jsonrpc: '2.0',
|
|
354
|
+
id: message[:id],
|
|
355
|
+
result: { tools: tools_data }
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def handle_mcp_tools_call(server, message, params)
|
|
360
|
+
# Execute tool on the SDK MCP server
|
|
361
|
+
tool_name = params[:name]
|
|
362
|
+
arguments = params[:arguments] || {}
|
|
363
|
+
|
|
364
|
+
# Call the tool
|
|
365
|
+
result = server.call_tool(tool_name, arguments)
|
|
366
|
+
|
|
367
|
+
# Format response
|
|
368
|
+
content = []
|
|
369
|
+
if result[:content]
|
|
370
|
+
result[:content].each do |item|
|
|
371
|
+
if item[:type] == 'text'
|
|
372
|
+
content << { type: 'text', text: item[:text] }
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
response_data = { content: content }
|
|
378
|
+
response_data[:is_error] = true if result[:is_error]
|
|
379
|
+
|
|
380
|
+
{
|
|
381
|
+
jsonrpc: '2.0',
|
|
382
|
+
id: message[:id],
|
|
383
|
+
result: response_data
|
|
384
|
+
}
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
public
|
|
388
|
+
|
|
389
|
+
# Send interrupt control request
|
|
390
|
+
def interrupt
|
|
391
|
+
send_control_request({ subtype: 'interrupt' })
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Change permission mode
|
|
395
|
+
def set_permission_mode(mode)
|
|
396
|
+
send_control_request({
|
|
397
|
+
subtype: 'set_permission_mode',
|
|
398
|
+
mode: mode
|
|
399
|
+
})
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Change the AI model
|
|
403
|
+
def set_model(model)
|
|
404
|
+
send_control_request({
|
|
405
|
+
subtype: 'set_model',
|
|
406
|
+
model: model
|
|
407
|
+
})
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Stream input messages to transport
|
|
411
|
+
def stream_input(stream)
|
|
412
|
+
stream.each do |message|
|
|
413
|
+
break if @closed
|
|
414
|
+
@transport.write(JSON.generate(message) + "\n")
|
|
415
|
+
end
|
|
416
|
+
@transport.end_input
|
|
417
|
+
rescue StandardError => e
|
|
418
|
+
# Log error but don't raise
|
|
419
|
+
warn "Error streaming input: #{e.message}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Receive SDK messages (not control messages)
|
|
423
|
+
def receive_messages(&block)
|
|
424
|
+
return enum_for(:receive_messages) unless block
|
|
425
|
+
|
|
426
|
+
loop do
|
|
427
|
+
message = @message_queue.dequeue
|
|
428
|
+
break if message[:type] == 'end'
|
|
429
|
+
raise message[:error] if message[:type] == 'error'
|
|
430
|
+
|
|
431
|
+
block.call(message)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Close the query and transport
|
|
436
|
+
def close
|
|
437
|
+
@closed = true
|
|
438
|
+
@task&.stop
|
|
439
|
+
@transport.close
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgentSDK
|
|
4
|
+
# SDK MCP Server - runs in-process within your Ruby application
|
|
5
|
+
#
|
|
6
|
+
# Unlike external MCP servers that run as separate processes, SDK MCP servers
|
|
7
|
+
# run directly in your application's process, providing better performance
|
|
8
|
+
# and simpler deployment.
|
|
9
|
+
class SdkMcpServer
|
|
10
|
+
attr_reader :name, :version, :tools
|
|
11
|
+
|
|
12
|
+
def initialize(name:, version: '1.0.0', tools: [])
|
|
13
|
+
@name = name
|
|
14
|
+
@version = version
|
|
15
|
+
@tools = tools
|
|
16
|
+
@tool_map = tools.each_with_object({}) { |tool, hash| hash[tool.name] = tool }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# List all available tools
|
|
20
|
+
# @return [Array<Hash>] Array of tool definitions
|
|
21
|
+
def list_tools
|
|
22
|
+
@tools.map do |tool|
|
|
23
|
+
{
|
|
24
|
+
name: tool.name,
|
|
25
|
+
description: tool.description,
|
|
26
|
+
inputSchema: convert_input_schema(tool.input_schema)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Execute a tool by name
|
|
32
|
+
# @param name [String] Tool name
|
|
33
|
+
# @param arguments [Hash] Tool arguments
|
|
34
|
+
# @return [Hash] Tool result
|
|
35
|
+
def call_tool(name, arguments)
|
|
36
|
+
tool = @tool_map[name]
|
|
37
|
+
raise "Tool '#{name}' not found" unless tool
|
|
38
|
+
|
|
39
|
+
# Call the tool's handler
|
|
40
|
+
result = tool.handler.call(arguments)
|
|
41
|
+
|
|
42
|
+
# Ensure result has the expected format
|
|
43
|
+
unless result.is_a?(Hash) && result[:content]
|
|
44
|
+
raise "Tool '#{name}' must return a hash with :content key"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def convert_input_schema(schema)
|
|
53
|
+
# If it's already a proper JSON schema, return it
|
|
54
|
+
if schema.is_a?(Hash) && schema[:type] && schema[:properties]
|
|
55
|
+
return schema
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Simple schema: hash mapping parameter names to types
|
|
59
|
+
if schema.is_a?(Hash)
|
|
60
|
+
properties = {}
|
|
61
|
+
schema.each do |param_name, param_type|
|
|
62
|
+
properties[param_name] = type_to_json_schema(param_type)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: properties,
|
|
68
|
+
required: properties.keys
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Default fallback
|
|
73
|
+
{ type: 'object', properties: {} }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def type_to_json_schema(type)
|
|
77
|
+
case type
|
|
78
|
+
when :string, String
|
|
79
|
+
{ type: 'string' }
|
|
80
|
+
when :integer, Integer
|
|
81
|
+
{ type: 'integer' }
|
|
82
|
+
when :float, Float
|
|
83
|
+
{ type: 'number' }
|
|
84
|
+
when :boolean, TrueClass, FalseClass
|
|
85
|
+
{ type: 'boolean' }
|
|
86
|
+
when :number
|
|
87
|
+
{ type: 'number' }
|
|
88
|
+
else
|
|
89
|
+
{ type: 'string' } # Default fallback
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Helper function to create a tool definition
|
|
95
|
+
#
|
|
96
|
+
# @param name [String] Unique identifier for the tool
|
|
97
|
+
# @param description [String] Human-readable description
|
|
98
|
+
# @param input_schema [Hash] Schema defining input parameters
|
|
99
|
+
# @param handler [Proc] Block that implements the tool logic
|
|
100
|
+
# @return [SdkMcpTool] Tool definition
|
|
101
|
+
#
|
|
102
|
+
# @example Simple tool
|
|
103
|
+
# tool = create_tool('greet', 'Greet a user', { name: :string }) do |args|
|
|
104
|
+
# { content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
|
|
105
|
+
# end
|
|
106
|
+
#
|
|
107
|
+
# @example Tool with multiple parameters
|
|
108
|
+
# tool = create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
|
|
109
|
+
# result = args[:a] + args[:b]
|
|
110
|
+
# { content: [{ type: 'text', text: "Result: #{result}" }] }
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# @example Tool with error handling
|
|
114
|
+
# tool = create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
|
|
115
|
+
# if args[:b] == 0
|
|
116
|
+
# { content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
|
|
117
|
+
# else
|
|
118
|
+
# result = args[:a] / args[:b]
|
|
119
|
+
# { content: [{ type: 'text', text: "Result: #{result}" }] }
|
|
120
|
+
# end
|
|
121
|
+
# end
|
|
122
|
+
def self.create_tool(name, description, input_schema, &handler)
|
|
123
|
+
raise ArgumentError, 'Block required for tool handler' unless handler
|
|
124
|
+
|
|
125
|
+
SdkMcpTool.new(
|
|
126
|
+
name: name,
|
|
127
|
+
description: description,
|
|
128
|
+
input_schema: input_schema,
|
|
129
|
+
handler: handler
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Create an SDK MCP server
|
|
134
|
+
#
|
|
135
|
+
# @param name [String] Unique identifier for the server
|
|
136
|
+
# @param version [String] Server version (default: '1.0.0')
|
|
137
|
+
# @param tools [Array<SdkMcpTool>] List of tool definitions
|
|
138
|
+
# @return [Hash] MCP server configuration for ClaudeAgentOptions
|
|
139
|
+
#
|
|
140
|
+
# @example Simple calculator server
|
|
141
|
+
# add_tool = ClaudeAgentSDK.create_tool('add', 'Add numbers', { a: :number, b: :number }) do |args|
|
|
142
|
+
# { content: [{ type: 'text', text: "Sum: #{args[:a] + args[:b]}" }] }
|
|
143
|
+
# end
|
|
144
|
+
#
|
|
145
|
+
# calculator = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
146
|
+
# name: 'calculator',
|
|
147
|
+
# version: '2.0.0',
|
|
148
|
+
# tools: [add_tool]
|
|
149
|
+
# )
|
|
150
|
+
#
|
|
151
|
+
# options = ClaudeAgentOptions.new(
|
|
152
|
+
# mcp_servers: { calc: calculator },
|
|
153
|
+
# allowed_tools: ['mcp__calc__add']
|
|
154
|
+
# )
|
|
155
|
+
def self.create_sdk_mcp_server(name:, version: '1.0.0', tools: [])
|
|
156
|
+
server = SdkMcpServer.new(name: name, version: version, tools: tools)
|
|
157
|
+
|
|
158
|
+
# Return configuration for ClaudeAgentOptions
|
|
159
|
+
{
|
|
160
|
+
type: 'sdk',
|
|
161
|
+
name: name,
|
|
162
|
+
instance: server
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
end
|