Class: Manceps::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/manceps/client.rb

Overview

MCP protocol client supporting tools, resources, prompts, and notifications.

Constant Summary collapse

MAX_PAGES =
100

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url_or_command, auth: Auth::None.new, args: nil, env: nil, max_retries: 3, **options) ⇒ Client

Returns a new instance of Client.



8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/manceps/client.rb', line 8

def initialize(url_or_command, auth: Auth::None.new, args: nil, env: nil, max_retries: 3, **options)
  @transport = if args || !url_or_command.match?(%r{\Ahttps?://}i)
                 Transport::Stdio.new(url_or_command, args: args || [], env: env || {})
               else
                 Transport::StreamableHTTP.new(url_or_command, auth: auth, timeout: options[:timeout])
               end
  @session = Session.new
  @max_retries = max_retries
  @backoff = Backoff.new
  @notification_handlers = Hash.new { |h, k| h[k] = [] }
  @elicitation_handler = nil
end

Instance Attribute Details

#sessionObject (readonly)

Returns the value of attribute session.



6
7
8
# File 'lib/manceps/client.rb', line 6

def session
  @session
end

Class Method Details

.open(url) ⇒ Object



195
196
197
198
199
200
201
# File 'lib/manceps/client.rb', line 195

def self.open(url, **)
  client = new(url, **)
  client.connect
  yield client
ensure
  client&.disconnect
end

Instance Method Details

#await_task(task_id, interval: 1, timeout: nil) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/manceps/client.rb', line 180

def await_task(task_id, interval: 1, timeout: nil)
  deadline = timeout ? Time.now + timeout : nil

  loop do
    task = get_task(task_id)
    return task if task.done?

    if deadline && Time.now >= deadline
      raise TimeoutError, "Task #{task_id} did not complete within #{timeout} seconds"
    end

    sleep interval
  end
end

#call_tool(name, **arguments) ⇒ Object



85
86
87
88
# File 'lib/manceps/client.rb', line 85

def call_tool(name, **arguments)
  response = request_with_retry('tools/call', name: name, arguments: arguments)
  ToolResult.new(response['result'])
end

#call_tool_streaming(name, **arguments, &block) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/manceps/client.rb', line 90

def call_tool_streaming(name, **arguments, &block)
  body = JsonRpc.request(@session.next_id, 'tools/call', { name: name, arguments: arguments })
  wrapped_block = if block
                    proc do |event|
                      if event.is_a?(Hash) && event['id'] && event['method']
                        handle_server_request(event)
                      else
                        block.call(event)
                      end
                    end
                  end
  response = @transport.request_streaming(body, &wrapped_block)
  handle_rpc_error(response)
  ToolResult.new(response['result'])
end

#cancel_request(request_id, reason: nil) ⇒ Object



140
141
142
143
144
# File 'lib/manceps/client.rb', line 140

def cancel_request(request_id, reason: nil)
  params = { requestId: request_id }
  params[:reason] = reason if reason
  @transport.notify(JsonRpc.notification('notifications/cancelled', params))
end

#cancel_task(task_id) ⇒ Object

rubocop:disable Naming/PredicateMethod



175
176
177
178
# File 'lib/manceps/client.rb', line 175

def cancel_task(task_id) # rubocop:disable Naming/PredicateMethod
  request('tasks/cancel', taskId: task_id)
  true
end

#connectObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/manceps/client.rb', line 21

def connect
  attempts = 0
  begin
    @transport.open if @transport.respond_to?(:open)

    init_response = @transport.request(
      JsonRpc.initialize_request(@session.next_id, capabilities: client_capabilities)
    )
    handle_rpc_error(init_response)
    @session.establish(init_response)

    unless Manceps.configuration.supported_versions.include?(@session.protocol_version)
      server_version = @session.protocol_version
      disconnect
      raise ProtocolError.new(
        "Server negotiated unsupported protocol version: #{server_version}",
        code: -32_600
      )
    end

    @transport.protocol_version = @session.protocol_version if @transport.respond_to?(:protocol_version=)

    @transport.notify(JsonRpc.initialized_notification)
    @backoff.reset
    self
  rescue ConnectionError, TimeoutError
    attempts += 1
    @transport.close if @transport.respond_to?(:close)
    @session.reset
    raise if attempts > @max_retries

    sleep @backoff.next_delay
    retry
  end
end

#connected?Boolean

Returns:

  • (Boolean)


64
65
66
# File 'lib/manceps/client.rb', line 64

def connected?
  @session.active?
end

#disconnectObject



57
58
59
60
61
62
# File 'lib/manceps/client.rb', line 57

def disconnect
  sid = @transport.respond_to?(:session_id) ? @transport.session_id : @session.id
  @transport.terminate_session(sid) if sid
  @transport.close
  @session.reset
end

#get_prompt(name, **arguments) ⇒ Object



110
111
112
113
# File 'lib/manceps/client.rb', line 110

def get_prompt(name, **arguments)
  response = request_with_retry('prompts/get', name: name, arguments: arguments)
  PromptResult.new(response['result'])
end

#get_task(task_id) ⇒ Object



170
171
172
173
# File 'lib/manceps/client.rb', line 170

def get_task(task_id)
  response = request('tasks/get', taskId: task_id)
  Task.new(response['result'])
end

#listenObject



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/manceps/client.rb', line 146

def listen
  @transport.listen do |message|
    if message['id'] && message['method']
      handle_server_request(message)
    else
      method = message['method']
      params = message['params']
      handlers = @notification_handlers[method]
      handlers&.each { |h| h.call(params) }
    end
  end
end

#on(method, &block) ⇒ Object



128
129
130
# File 'lib/manceps/client.rb', line 128

def on(method, &block)
  @notification_handlers[method] << block
end

#on_elicitation(&block) ⇒ Object



159
160
161
# File 'lib/manceps/client.rb', line 159

def on_elicitation(&block)
  @elicitation_handler = block
end

#pingObject



74
75
76
77
78
79
# File 'lib/manceps/client.rb', line 74

def ping
  @transport.notify(JsonRpc.notification('ping'))
  true
rescue ConnectionError, TimeoutError
  false
end

#prompts(force: false) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



106
107
108
# File 'lib/manceps/client.rb', line 106

def prompts(force: false) # rubocop:disable Lint/UnusedMethodArgument
  paginate_with_retry('prompts/list', 'prompts') { |data| Prompt.new(data) }
end

#read_resource(uri) ⇒ Object



123
124
125
126
# File 'lib/manceps/client.rb', line 123

def read_resource(uri)
  response = request_with_retry('resources/read', uri: uri)
  ResourceContents.new(response['result'])
end

#reconnect!Object



68
69
70
71
72
# File 'lib/manceps/client.rb', line 68

def reconnect!
  @transport.close
  @session.reset
  connect
end

#resource_templatesObject



119
120
121
# File 'lib/manceps/client.rb', line 119

def resource_templates
  paginate_with_retry('resources/templates/list', 'resourceTemplates') { |data| ResourceTemplate.new(data) }
end

#resources(force: false) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



115
116
117
# File 'lib/manceps/client.rb', line 115

def resources(force: false) # rubocop:disable Lint/UnusedMethodArgument
  paginate_with_retry('resources/list', 'resources') { |data| Resource.new(data) }
end

#subscribe_resource(uri) ⇒ Object



132
133
134
# File 'lib/manceps/client.rb', line 132

def subscribe_resource(uri)
  request('resources/subscribe', uri: uri)
end

#tasksObject

— Tasks (experimental, protocol 2025-11-25) —



165
166
167
168
# File 'lib/manceps/client.rb', line 165

def tasks
  response = request('tasks/list')
  (response.dig('result', 'tasks') || []).map { |data| Task.new(data) }
end

#tools(force: false) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



81
82
83
# File 'lib/manceps/client.rb', line 81

def tools(force: false) # rubocop:disable Lint/UnusedMethodArgument
  paginate_with_retry('tools/list', 'tools') { |data| Tool.new(data) }
end

#unsubscribe_resource(uri) ⇒ Object



136
137
138
# File 'lib/manceps/client.rb', line 136

def unsubscribe_resource(uri)
  request('resources/unsubscribe', uri: uri)
end