Class: ActionMCP::Session::Task
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- ActionMCP::Session::Task
- Defined in:
- app/models/action_mcp/session/task.rb
Overview
Represents a Task in an MCP session as per MCP 2025-11-25 specification. Tasks provide durable state machines for tracking async request execution.
State Machine:
working -> input_required -> working (via resume)
working -> completed | failed | cancelled
input_required -> completed | failed | cancelled
Constant Summary collapse
- RELATED_TASK_META_KEY =
"io.modelcontextprotocol/related-task"
Instance Method Summary collapse
-
#await_input!(prompt:, context: {}) ⇒ Object
Transition to input_required state and store pending input prompt.
-
#broadcast_status_change(transition = nil) ⇒ Object
Broadcast status change notification to the session.
-
#expired? ⇒ Boolean
TTL management.
-
#non_terminal? ⇒ Boolean
Check if task is in a non-terminal state.
-
#record_step!(step_name, cursor: nil, data: {}) ⇒ Object
Record step execution state for job resumption.
- #related_task_meta ⇒ Object
- #request_meta_with_related_task(meta = nil) ⇒ Object
- #result_ready? ⇒ Boolean
-
#resume_from_continuation! ⇒ void
Resume task from input_required state and re-enqueue job.
-
#store_partial_result!(result_fragment) ⇒ Object
Store partial result fragment (for streaming/incremental results).
-
#terminal? ⇒ Boolean
Check if task is in a terminal state.
- #to_create_task_result ⇒ Object
-
#to_task_data ⇒ Hash
Convert to task data format per MCP spec.
- #to_task_error ⇒ Object
-
#to_task_result ⇒ Hash
Convert to the original request’s result payload for tasks/result.
-
#update_progress!(percent:, message: nil) ⇒ Object
Update progress indicators for long-running tasks.
Instance Method Details
#await_input!(prompt:, context: {}) ⇒ Object
Transition to input_required state and store pending input prompt
276 277 278 279 |
# File 'app/models/action_mcp/session/task.rb', line 276 def await_input!(prompt:, context: {}) record_step!(:awaiting_input, data: { prompt: prompt, context: context }) require_input! end |
#broadcast_status_change(transition = nil) ⇒ Object
Broadcast status change notification to the session
226 227 228 229 230 231 232 233 |
# File 'app/models/action_mcp/session/task.rb', line 226 def broadcast_status_change(transition = nil) return unless session handler = ActionMCP::Server::TransportHandler.new(session) handler.send_task_status_notification(self) rescue StandardError => e Rails.logger.warn "Failed to broadcast task status change: #{e.}" end |
#expired? ⇒ Boolean
TTL management
132 133 134 135 136 137 |
# File 'app/models/action_mcp/session/task.rb', line 132 def expired? return false if ttl.nil? # TTL is stored in milliseconds (MCP spec) created_at + (ttl / 1000.0).seconds < Time.current end |
#non_terminal? ⇒ Boolean
Check if task is in a non-terminal state
149 150 151 |
# File 'app/models/action_mcp/session/task.rb', line 149 def non_terminal? !terminal? end |
#record_step!(step_name, cursor: nil, data: {}) ⇒ Object
Record step execution state for job resumption
241 242 243 244 245 246 247 248 249 250 251 |
# File 'app/models/action_mcp/session/task.rb', line 241 def record_step!(step_name, cursor: nil, data: {}) update!( continuation_state: { step: step_name, cursor: cursor, data: data, timestamp: Time.current.iso8601 }, last_step_at: Time.current ) end |
#related_task_meta ⇒ Object
205 206 207 208 209 210 211 |
# File 'app/models/action_mcp/session/task.rb', line 205 def { RELATED_TASK_META_KEY => { "taskId" => id } } end |
#request_meta_with_related_task(meta = nil) ⇒ Object
213 214 215 216 217 218 219 220 221 222 |
# File 'app/models/action_mcp/session/task.rb', line 213 def ( = nil) = if .respond_to?(:to_h) .to_h.deep_dup else {} end .deep_merge() end |
#result_ready? ⇒ Boolean
144 145 146 |
# File 'app/models/action_mcp/session/task.rb', line 144 def result_ready? terminal? || input_required? end |
#resume_from_continuation! ⇒ void
This method returns an undefined value.
Resume task from input_required state and re-enqueue job
283 284 285 286 287 288 289 |
# File 'app/models/action_mcp/session/task.rb', line 283 def resume_from_continuation! return unless input_required? resume! # Re-enqueue the job to continue execution ActionMCP::ToolExecutionJob.perform_later(id, request_name, request_params, {}) end |
#store_partial_result!(result_fragment) ⇒ Object
Store partial result fragment (for streaming/incremental results)
255 256 257 258 259 260 |
# File 'app/models/action_mcp/session/task.rb', line 255 def store_partial_result!(result_fragment) payload = result_payload || {} payload[:partial] ||= [] payload[:partial] << result_fragment update!(result_payload: payload) end |
#terminal? ⇒ Boolean
Check if task is in a terminal state
140 141 142 |
# File 'app/models/action_mcp/session/task.rb', line 140 def terminal? status.in?(%w[completed failed cancelled]) end |
#to_create_task_result ⇒ Object
169 170 171 172 173 174 |
# File 'app/models/action_mcp/session/task.rb', line 169 def to_create_task_result { task: to_task_data, _meta: } end |
#to_task_data ⇒ Hash
Convert to task data format per MCP spec
155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'app/models/action_mcp/session/task.rb', line 155 def to_task_data data = { taskId: id, status: status, createdAt: created_at.iso8601(3), lastUpdatedAt: last_updated_at.iso8601(3), ttl: ttl } data[:statusMessage] = if .present? data[:pollInterval] = poll_interval if poll_interval.present? data end |
#to_task_error ⇒ Object
190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'app/models/action_mcp/session/task.rb', line 190 def to_task_error return unless result_payload.is_a?(Hash) code = result_payload["code"] || result_payload[:code] = result_payload["message"] || result_payload[:message] return unless code && return if result_payload.key?("content") || result_payload.key?(:content) return if result_payload.key?("isError") || result_payload.key?(:isError) error = { code: code, message: } data = result_payload["data"] || result_payload[:data] error[:data] = data unless data.nil? error end |
#to_task_result ⇒ Hash
Convert to the original request’s result payload for tasks/result. The result carries related-task metadata because its structure does not otherwise include the task identifier.
180 181 182 183 184 185 186 187 188 |
# File 'app/models/action_mcp/session/task.rb', line 180 def to_task_result payload = result_payload.is_a?(Hash) ? result_payload.deep_dup : {} = payload.delete("_meta") || payload.delete(:_meta) || {} = .to_h if .respond_to?(:to_h) = {} unless .is_a?(Hash) payload[:_meta] = .deep_merge() payload end |
#update_progress!(percent:, message: nil) ⇒ Object
Update progress indicators for long-running tasks
265 266 267 268 269 270 271 |
# File 'app/models/action_mcp/session/task.rb', line 265 def update_progress!(percent:, message: nil) update!( progress_percent: percent.clamp(0, 100), progress_message: , last_step_at: Time.current ) end |