Class: MCPClient::Task

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

Parameters:

  • task_id (String) —

    unique task identifier

  • status (String) (defaults to: 'working') —

    task status (working, input_required, completed, failed, cancelled)

  • status_message (String, nil) (defaults to: nil) —

    optional human-readable status detail

  • created_at (String, nil) (defaults to: nil) —

    ISO 8601 creation timestamp

  • last_updated_at (String, nil) (defaults to: nil) —

    ISO 8601 last-update timestamp

  • ttl (Integer, nil) (defaults to: nil) —

    retention duration in milliseconds since creation (nil = unspecified)

  • poll_interval (Integer, nil) (defaults to: nil) —

    suggested polling interval in milliseconds

  • server (MCPClient::ServerBase, nil) (defaults to: nil) —

    the server this task belongs to

  • input_requests (Hash, nil) (defaults to: nil) —

    outstanding inputRequests (2026-07-28 DetailedTask, input_required)

  • result (Hash, nil) (defaults to: nil) —

    the final result (2026-07-28 DetailedTask, completed)

  • error (Hash, nil) (defaults to: nil) —

    the JSON-RPC error (2026-07-28 DetailedTask, failed)

  • modern (Boolean) (defaults to: false) —

    whether the task uses the 2026-07-28 field names (ttlMs, pollIntervalMs)

  • detailed (Boolean) (defaults to: false) —

    whether this is a DetailedTask (tasks/get, notifications/tasks) whose result / error / inputRequests are authoritative, as opposed to a creation seed

  • ttl_reported (Boolean, nil) (defaults to: nil) —

    whether the source hash carried the ttl / ttlMs field at all, whatever its value (nil: derived from ttl, for a task not built from peer data)

  • session_epoch (Integer, nil) (defaults to: nil) —

    the server session this handle is about: the session the request that produced it was pinned to, which is not necessarily the one that is live by the time the handle is built (nil: sampled from the server, for a handle built outside a request)

  • task_generation (Integer, nil) (defaults to: nil) —

    which task under this id the handle names, for a handle built from a CreateTaskResult: a task id is unique within a session, so a later creation under the same id ends this task (nil: a handle that names whatever the id means now)



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 = 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
  @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).

Parameters:

  • result (Object)

Returns:

  • (Boolean)


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.

Parameters:

Returns:

  • (Task) —

    a completed task carrying the result



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.

Parameters:

  • result (Hash) —

    the CreateTaskResult ({ 'task' => { ... } })

  • server (MCPClient::ServerBase, nil) (defaults to: nil) —

    optional server reference

  • session_epoch (Integer, nil) (defaults to: nil) —

    the server session the creating request was pinned to (see #initialize)

Returns:



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.

Parameters:

  • json (Hash) —

    the flat task hash

  • server (MCPClient::ServerBase, nil) (defaults to: nil) —

    optional server reference

  • detailed (Boolean) (defaults to: false) —

    whether the hash is a DetailedTask (see #detailed?)

  • session_epoch (Integer, nil) (defaults to: nil) —

    the server session the request that returned this hash was pinned to (see #initialize)

  • task_generation (Integer, nil) (defaults to: nil) —

    which task under this id the hash describes, for a CreateTaskResult (see #initialize)

Returns:

Raises:



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.message}"
end

.jsonrpc_error_object?(error) ⇒ Boolean

Returns whether it is a JSON-RPC error object (integer code, string message).

Parameters:

  • error (Object)

Returns:

  • (Boolean) —

    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]
  message = error['message'] || error[:message]
  code.is_a?(Integer) && message.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)

Returns:

  • (Boolean)


361
362
363
# File 'lib/mcp_client/task.rb', line 361

def active?
  !terminal?
end

#cancelled? ⇒ Boolean

Returns whether the task was cancelled.

Returns:

  • (Boolean) —

    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).

Returns:

  • (Boolean) —

    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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean) —

    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)

Returns:

  • (Boolean)


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).

Parameters:

  • error (Object)

Returns:

  • (Boolean)


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).

Returns:

  • (Boolean)


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).

Returns:

  • (Boolean)


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).

Returns:

  • (Boolean)


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)

Returns:

  • (Boolean)


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

Returns:

  • (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

Returns:

  • (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".

Parameters:

  • now (Time) (defaults to: Time.now) —

    the current time

Returns:

  • (Boolean) —

    whether createdAt + ttl has passed (false when unknown or unlimited)



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.

Parameters:

  • now (Time) (defaults to: Time.now)

Returns:

  • (Float, nil)


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.

Returns:

  • (Boolean)


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.

Parameters:

  • tool (MCPClient::Tool, nil) —

    the definition the creating tools/call carried

Returns:



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.

Parameters:

  • generation (Integer, nil) —

    the lifetime the handle names

Returns:



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)

Returns:

  • (Boolean)


373
374
375
# File 'lib/mcp_client/task.rb', line 373

def working?
  @status == 'working'
end