Class: MCPClient::Task
- Inherits:
-
Object
- Object
- MCPClient::Task
- Defined in:
- lib/mcp_client/task.rb
Overview
Represents an MCP Task for long-running, task-augmented operations.
Conforms to the MCP 2025-11-25 Tasks utility and to the MCP 2026-07-28
tasks extension (io.modelcontextprotocol/tasks), whose flat Task uses
ttlMs / pollIntervalMs and whose DetailedTask (tasks/get,
notifications/tasks) inlines inputRequests, result or error.
Task statuses: working, input_required, completed, failed, cancelled.
A task begins in working; completed/failed/cancelled are terminal.
Constant Summary collapse
- VALID_STATUSES =
Valid task statuses (MCP 2025-11-25)
%w[working input_required completed failed cancelled].freeze
- TERMINAL_STATUSES =
Statuses from which a task will not transition further
%w[completed failed cancelled].freeze
Instance Attribute Summary collapse
-
#called_tool ⇒ Object
readonly
Returns the value of attribute called_tool.
-
#created_at ⇒ Object
readonly
Returns the value of attribute created_at.
-
#error ⇒ Object
readonly
Returns the value of attribute error.
-
#input_requests ⇒ Object
readonly
Returns the value of attribute input_requests.
-
#last_updated_at ⇒ Object
readonly
Returns the value of attribute last_updated_at.
-
#poll_interval ⇒ Object
(also: #poll_interval_ms)
readonly
Returns the value of attribute poll_interval.
-
#result ⇒ Object
readonly
Returns the value of attribute result.
-
#server ⇒ Object
readonly
Returns the value of attribute server.
-
#session_epoch ⇒ Object
readonly
Returns the value of attribute session_epoch.
-
#status ⇒ Object
readonly
Returns the value of attribute status.
-
#status_message ⇒ Object
readonly
Returns the value of attribute status_message.
-
#task_generation ⇒ Object
readonly
Returns the value of attribute task_generation.
-
#task_id ⇒ Object
readonly
Returns the value of attribute task_id.
-
#ttl ⇒ Object
(also: #ttl_ms)
readonly
Returns the value of attribute ttl.
Class Method Summary collapse
-
.complete_result_object?(result) ⇒ Boolean
A completed task's result is the final result of the original request (a CallToolResult): an object that is a complete result, so a resultType it carries must be "complete" (or absent).
-
.completed_locally(result, server: nil) ⇒ Task
A task that never left the client: the server answered the request synchronously, so there is nothing to poll.
-
.from_create_result(result, server: nil, session_epoch: nil) ⇒ Task
Build a Task from a CreateTaskResult, which wraps the task under
task. -
.from_json(json, server: nil, detailed: false, session_epoch: nil, task_generation: nil) ⇒ Task
Build a Task from a flat Task hash.
-
.jsonrpc_error_object?(error) ⇒ Boolean
Whether it is a JSON-RPC error object (integer code, string message).
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
Check equality.
-
#active? ⇒ Boolean
Whether the task is still active (not terminal — working or input_required).
-
#cancelled? ⇒ Boolean
Whether the task was cancelled.
-
#completed? ⇒ Boolean
Whether the task completed (its result is available).
-
#detailed? ⇒ Boolean
Whether the task came from tasks/get or notifications/tasks (a DetailedTask, whose result, error and inputRequests are authoritative) rather than from the CreateTaskResult seed, which carries none of them.
-
#failed? ⇒ Boolean
Whether the task failed with a JSON-RPC error.
- #hash ⇒ Object
-
#initialize(task_id:, status: 'working', status_message: nil, created_at: nil, last_updated_at: nil, ttl: nil, poll_interval: nil, server: nil, input_requests: nil, result: nil, error: nil, modern: false, detailed: false, ttl_reported: nil, session_epoch: nil, task_generation: nil) ⇒ Task
constructor
Create a new Task.
-
#input_required? ⇒ Boolean
Whether the task is waiting for input (status input_required).
- #inspect ⇒ Object
-
#jsonrpc_error_object?(error) ⇒ Boolean
Whether a failed task's error is a JSON-RPC error object ("The request failed due to a JSON-RPC error": an integer code and a string message, as the JSON-RPC error shape requires).
-
#modern? ⇒ Boolean
Whether the task uses the 2026-07-28 shape (ttlMs / pollIntervalMs).
-
#payload_present? ⇒ Boolean
Whether the terminal payload the status implies is present and well formed: a result object (a CallToolResult) for completed, an error object for failed (cancelled needs none).
-
#remote? ⇒ Boolean
Whether this task exists on the server (has an id) as opposed to a request the server answered synchronously (see .completed_locally).
-
#terminal? ⇒ Boolean
Whether the task is in a terminal status (completed, failed, cancelled).
-
#to_h ⇒ Hash
Convert to a spec-shaped, JSON-serializable hash.
-
#to_json ⇒ String
Convert to JSON string.
-
#to_s ⇒ Object
String representation.
-
#ttl_elapsed?(now: Time.now) ⇒ Boolean
MCP 2026-07-28 TTL backstop: "if the task's observable status has not reflected the update after createdAt plus ttlMs has elapsed, the client MAY consider the task to no longer be usable".
-
#ttl_remaining(now: Time.now) ⇒ Float?
Seconds left before the TTL backstop (createdAt + ttlMs), nil when unknown or unlimited.
-
#ttl_reported? ⇒ Boolean
Whether the observation this task was built from carried a ttl / ttlMs field at all.
-
#with_called_tool(tool) ⇒ Task
A copy of this handle naming the tool definition the request that created the task went out under, so the result the task delivers can be validated against the very schema a synchronous answer would have been (see Client#get_task_result).
-
#with_task_generation(generation) ⇒ Task
A copy of this handle naming a definite lifetime of its (reusable) task id: the task one CreateTaskResult started, as opposed to whatever the id means later (see Client::TaskRegistry).
-
#working? ⇒ Boolean
Whether the task is still running (status working).
Constructor Details
#initialize(task_id:, status: 'working', status_message: nil, created_at: nil, last_updated_at: nil, ttl: nil, poll_interval: nil, server: nil, input_requests: nil, result: nil, error: nil, modern: false, detailed: false, ttl_reported: nil, session_epoch: nil, task_generation: nil) ⇒ Task
Create a new Task
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/mcp_client/task.rb', line 52 def initialize(task_id:, status: 'working', status_message: nil, created_at: nil, last_updated_at: nil, ttl: nil, poll_interval: nil, server: nil, input_requests: nil, result: nil, error: nil, modern: false, detailed: false, ttl_reported: nil, session_epoch: nil, task_generation: nil) validate_status!(status) @task_id = task_id @status = status @status_message = @created_at = created_at @last_updated_at = last_updated_at @ttl = ttl # An explicit null ttlMs is a reported TTL (an unlimited one); a hash # without the field reports nothing, so an observation of it must not # be read as the server lifting a TTL it never mentioned. @ttl_reported = ttl_reported.nil? ? !ttl.nil? : ttl_reported @poll_interval = poll_interval @server = server # The server session this handle was seen in: task ids are per session # and reusable, so what a handle kept across a restart says (its TTL # backstop, its polling interval) is about a task that no longer # exists. nil for a server that reports no session. # # It is the session the request that produced the handle was pinned to, # passed in by the caller that sent it: sampling the server here would # stamp a handle built from an answer of the session that has just # ended with the session that replaced it, whose task-1 is another task. @session_epoch = session_epoch || (server.respond_to?(:session_epoch) ? server.session_epoch : nil) # Which task under this (reusable) id the handle names; see the tasks # extension's per-creation lifetime in {MCPClient::Client::TaskRegistry}. @task_generation = task_generation @input_requests = input_requests @result = result @error = error @modern = modern @detailed = detailed end |
Instance Attribute Details
#called_tool ⇒ Object (readonly)
Returns the value of attribute called_tool.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def called_tool @called_tool end |
#created_at ⇒ Object (readonly)
Returns the value of attribute created_at.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def created_at @created_at end |
#error ⇒ Object (readonly)
Returns the value of attribute error.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def error @error end |
#input_requests ⇒ Object (readonly)
Returns the value of attribute input_requests.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def input_requests @input_requests end |
#last_updated_at ⇒ Object (readonly)
Returns the value of attribute last_updated_at.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def last_updated_at @last_updated_at end |
#poll_interval ⇒ Object (readonly) Also known as: poll_interval_ms
Returns the value of attribute poll_interval.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def poll_interval @poll_interval end |
#result ⇒ Object (readonly)
Returns the value of attribute result.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def result @result end |
#server ⇒ Object (readonly)
Returns the value of attribute server.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def server @server end |
#session_epoch ⇒ Object (readonly)
Returns the value of attribute session_epoch.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def session_epoch @session_epoch end |
#status ⇒ Object (readonly)
Returns the value of attribute status.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def status @status end |
#status_message ⇒ Object (readonly)
Returns the value of attribute status_message.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def @status_message end |
#task_generation ⇒ Object (readonly)
Returns the value of attribute task_generation.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def task_generation @task_generation end |
#task_id ⇒ Object (readonly)
Returns the value of attribute task_id.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def task_id @task_id end |
#ttl ⇒ Object (readonly) Also known as: ttl_ms
Returns the value of attribute ttl.
22 23 24 |
# File 'lib/mcp_client/task.rb', line 22 def ttl @ttl end |
Class Method Details
.complete_result_object?(result) ⇒ Boolean
A completed task's result is the final result of the original request (a CallToolResult): an object that is a complete result, so a resultType it carries must be "complete" (or absent).
253 254 255 256 257 258 259 260 |
# File 'lib/mcp_client/task.rb', line 253 def self.complete_result_object?(result) return false unless result.is_a?(Hash) # Only an absent discriminator gets the compatibility default; a # present null is an unrecognized result type. key = ['resultType', :resultType].find { |k| result.key?(k) } key.nil? || result[key] == 'complete' end |
.completed_locally(result, server: nil) ⇒ Task
A task that never left the client: the server answered the request synchronously, so there is nothing to poll. It has no task id.
143 144 145 |
# File 'lib/mcp_client/task.rb', line 143 def self.completed_locally(result, server: nil) new(task_id: nil, status: 'completed', result: result, server: server, modern: true, detailed: true) end |
.from_create_result(result, server: nil, session_epoch: nil) ⇒ Task
Build a Task from a CreateTaskResult, which wraps the task under task.
163 164 165 166 |
# File 'lib/mcp_client/task.rb', line 163 def self.from_create_result(result, server: nil, session_epoch: nil) task_data = (result && (result['task'] || result[:task])) || result from_json(task_data, server: server, session_epoch: session_epoch) end |
.from_json(json, server: nil, detailed: false, session_epoch: nil, task_generation: nil) ⇒ Task
Build a Task from a flat Task hash. This is the shape of GetTaskResult, CancelTaskResult, the items in a ListTasksResult, and the params of a notifications/tasks/status notification.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/mcp_client/task.rb', line 102 def self.from_json(json, server: nil, detailed: false, session_epoch: nil, task_generation: nil) raise MCPClient::Errors::InvalidResultError, 'Invalid task: not an object' unless json.is_a?(Hash) data = json modern = modern_shape?(data) new( task_id: extract_field(data, 'taskId', :task_id), status: extract_field(data, 'status') || 'working', status_message: extract_field(data, 'statusMessage', :status_message), created_at: extract_field(data, 'createdAt', :created_at), last_updated_at: extract_field(data, 'lastUpdatedAt', :last_updated_at), ttl: modern ? extract_field(data, 'ttlMs', :ttl_ms) : extract_field(data, 'ttl'), ttl_reported: modern ? field_present?(data, 'ttlMs', :ttl_ms) : field_present?(data, 'ttl'), poll_interval: if modern extract_field(data, 'pollIntervalMs', :poll_interval_ms) else extract_field(data, 'pollInterval', :poll_interval) end, input_requests: extract_field(data, 'inputRequests', :input_requests), result: extract_field(data, 'result'), error: extract_field(data, 'error'), modern: modern, # A hash carrying what only a DetailedTask carries (a result, an # error, the input requests) is one, however it was handed back: a # host that persisted a completed handle's #to_h reads its result # from it instead of asking a server that may have purged the task. detailed: detailed || detail_carried?(data), server: server, session_epoch: session_epoch, task_generation: task_generation ) rescue ArgumentError => e raise MCPClient::Errors::InvalidResultError, "Invalid task: #{e.}" end |
.jsonrpc_error_object?(error) ⇒ Boolean
Returns whether it is a JSON-RPC error object (integer code, string message).
264 265 266 267 268 269 270 |
# File 'lib/mcp_client/task.rb', line 264 def self.jsonrpc_error_object?(error) return false unless error.is_a?(Hash) code = error['code'] || error[:code] = error['message'] || error[:message] code.is_a?(Integer) && .is_a?(String) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
Check equality
393 394 395 396 397 398 399 |
# File 'lib/mcp_client/task.rb', line 393 def ==(other) return false unless other.is_a?(Task) # A locally completed task has no server-side identity: only itself. return equal?(other) if task_id.nil? task_id == other.task_id && status == other.status end |
#active? ⇒ Boolean
Whether the task is still active (not terminal — working or input_required)
361 362 363 |
# File 'lib/mcp_client/task.rb', line 361 def active? !terminal? end |
#cancelled? ⇒ Boolean
Returns whether the task was cancelled.
388 389 390 |
# File 'lib/mcp_client/task.rb', line 388 def cancelled? @status == 'cancelled' end |
#completed? ⇒ Boolean
Returns whether the task completed (its result is available).
378 379 380 |
# File 'lib/mcp_client/task.rb', line 378 def completed? @status == 'completed' end |
#detailed? ⇒ Boolean
Whether the task came from tasks/get or notifications/tasks (a DetailedTask, whose result, error and inputRequests are authoritative) rather than from the CreateTaskResult seed, which carries none of them.
223 224 225 |
# File 'lib/mcp_client/task.rb', line 223 def detailed? @detailed end |
#failed? ⇒ Boolean
Returns whether the task failed with a JSON-RPC error.
383 384 385 |
# File 'lib/mcp_client/task.rb', line 383 def failed? @status == 'failed' end |
#hash ⇒ Object
403 404 405 406 407 |
# File 'lib/mcp_client/task.rb', line 403 def hash return object_id.hash if task_id.nil? [task_id, status].hash end |
#input_required? ⇒ Boolean
Whether the task is waiting for input (status input_required)
367 368 369 |
# File 'lib/mcp_client/task.rb', line 367 def input_required? @status == 'input_required' end |
#inspect ⇒ Object
416 417 418 |
# File 'lib/mcp_client/task.rb', line 416 def inspect "#<MCPClient::Task task_id=#{@task_id.inspect} status=#{@status.inspect}>" end |
#jsonrpc_error_object?(error) ⇒ Boolean
Whether a failed task's error is a JSON-RPC error object ("The request failed due to a JSON-RPC error": an integer code and a string message, as the JSON-RPC error shape requires).
244 245 246 |
# File 'lib/mcp_client/task.rb', line 244 def jsonrpc_error_object?(error) self.class.jsonrpc_error_object?(error) end |
#modern? ⇒ Boolean
Whether the task uses the 2026-07-28 shape (ttlMs / pollIntervalMs).
215 216 217 |
# File 'lib/mcp_client/task.rb', line 215 def modern? @modern end |
#payload_present? ⇒ Boolean
Whether the terminal payload the status implies is present and well formed: a result object (a CallToolResult) for completed, an error object for failed (cancelled needs none).
231 232 233 234 235 236 237 |
# File 'lib/mcp_client/task.rb', line 231 def payload_present? case @status when 'completed' then self.class.complete_result_object?(@result) when 'failed' then jsonrpc_error_object?(@error) else terminal? end end |
#remote? ⇒ Boolean
Whether this task exists on the server (has an id) as opposed to a request the server answered synchronously (see .completed_locally).
327 328 329 |
# File 'lib/mcp_client/task.rb', line 327 def remote? !@task_id.nil? end |
#terminal? ⇒ Boolean
Whether the task is in a terminal status (completed, failed, cancelled)
355 356 357 |
# File 'lib/mcp_client/task.rb', line 355 def terminal? TERMINAL_STATUSES.include?(@status) end |
#to_h ⇒ Hash
Convert to a spec-shaped, JSON-serializable hash
198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/mcp_client/task.rb', line 198 def to_h # ttl / ttlMs is a REQUIRED Task field whose value may be null, so it is # always included (even when nil). The other optional fields are omitted # when nil. hash = { 'taskId' => @task_id, 'status' => @status, (@modern ? 'ttlMs' : 'ttl') => @ttl } hash['statusMessage'] = @status_message if @status_message hash['createdAt'] = @created_at if @created_at hash['lastUpdatedAt'] = @last_updated_at if @last_updated_at hash[@modern ? 'pollIntervalMs' : 'pollInterval'] = @poll_interval if @poll_interval hash['inputRequests'] = @input_requests if @input_requests hash['result'] = @result unless @result.nil? hash['error'] = @error if @error hash end |
#to_json ⇒ String
Convert to JSON string
349 350 351 |
# File 'lib/mcp_client/task.rb', line 349 def to_json(*) to_h.to_json(*) end |
#to_s ⇒ Object
String representation
410 411 412 413 414 |
# File 'lib/mcp_client/task.rb', line 410 def to_s parts = ["Task[#{@task_id}]: #{@status}"] parts << "- #{@status_message}" if @status_message parts.join(' ') end |
#ttl_elapsed?(now: Time.now) ⇒ Boolean
MCP 2026-07-28 TTL backstop: "if the task's observable status has not reflected the update after createdAt plus ttlMs has elapsed, the client MAY consider the task to no longer be usable".
336 337 338 339 340 341 342 343 344 345 |
# File 'lib/mcp_client/task.rb', line 336 def ttl_elapsed?(now: Time.now) return false unless @ttl.is_a?(Numeric) && @created_at.is_a?(String) created = Time.iso8601(@created_at) now > created + (@ttl / 1000.0) rescue ArgumentError, RangeError, TypeError # FloatDomainError is a RangeError # Like #ttl_remaining: an unparseable timestamp or an overflowing # ttlMs is no backstop, never a raw exception for a host that polls. false end |
#ttl_remaining(now: Time.now) ⇒ Float?
Seconds left before the TTL backstop (createdAt + ttlMs), nil when unknown or unlimited.
286 287 288 289 290 291 292 293 294 |
# File 'lib/mcp_client/task.rb', line 286 def ttl_remaining(now: Time.now) return nil unless @ttl.is_a?(Numeric) && @created_at.is_a?(String) (Time.iso8601(@created_at) + (@ttl / 1000.0)) - now rescue ArgumentError, RangeError, TypeError # FloatDomainError is a RangeError # An unparseable timestamp, or a peer-supplied ttlMs too large for a # Time: no backstop, never a raw exception out of a poll. nil end |
#ttl_reported? ⇒ Boolean
Whether the observation this task was built from carried a ttl / ttlMs field at all. It separates "no TTL reported" from a reported TTL that yields no deadline (an explicit null, or a value the clock cannot represent): both of the latter mean the task has no backstop, while the former says nothing about one.
278 279 280 |
# File 'lib/mcp_client/task.rb', line 278 def ttl_reported? @ttl_reported end |
#with_called_tool(tool) ⇒ Task
A copy of this handle naming the tool definition the request that created the task went out under, so the result the task delivers can be validated against the very schema a synchronous answer would have been (see Client#get_task_result). A task id alone identifies no tool, so only a handle carries this.
316 317 318 319 320 321 322 |
# File 'lib/mcp_client/task.rb', line 316 def with_called_tool(tool) return self unless tool copy = dup copy.instance_variable_set(:@called_tool, tool) copy end |
#with_task_generation(generation) ⇒ Task
A copy of this handle naming a definite lifetime of its (reusable) task id: the task one CreateTaskResult started, as opposed to whatever the id means later (see Client::TaskRegistry). Used by the creation paths that build the handle before the lifetime it starts is counted.
303 304 305 306 307 |
# File 'lib/mcp_client/task.rb', line 303 def with_task_generation(generation) copy = dup copy.instance_variable_set(:@task_generation, generation) copy end |
#working? ⇒ Boolean
Whether the task is still running (status working)
373 374 375 |
# File 'lib/mcp_client/task.rb', line 373 def working? @status == 'working' end |