ExMCP.Protocol.RequestTracker (ex_mcp v0.9.0)
View SourceManages tracking of pending requests and cancellations for MCP servers.
This module provides functionality to:
- Track pending requests with their associated GenServer from references
- Track cancelled request IDs
- Handle request completion and cancellation
- Query pending request status
The tracker maintains two data structures:
pending_requests: Map of request_id => from (GenServer reference)cancelled_requests: MapSet of cancelled request_ids
Summary
Functions
Marks a request as cancelled.
Checks if a request has been cancelled.
Completes a pending request by removing it from tracking.
Gets the from reference for a pending request.
Gets all pending request IDs.
Handles a cancellation notification.
Initializes request tracker state.
Merges request tracker state into existing state map.
Processes a request with cancellation checking.
Tracks a new pending request.
Types
Functions
@spec cancel_request(request_id(), state()) :: state()
Marks a request as cancelled.
Adds the request_id to the cancelled_requests set.
Examples
iex> state = RequestTracker.init()
iex> new_state = RequestTracker.cancel_request("req-123", state)
iex> RequestTracker.cancelled?("req-123", new_state)
true
@spec cancelled?(request_id(), state()) :: boolean()
Checks if a request has been cancelled.
Examples
iex> state = %{cancelled_requests: MapSet.new(["req-123"])}
iex> RequestTracker.cancelled?("req-123", state)
true
iex> RequestTracker.cancelled?("req-456", state)
false
@spec complete_request(request_id(), state()) :: state()
Completes a pending request by removing it from tracking.
Examples
iex> state = %{pending_requests: %{"req-123" => {self(), :tag}}, cancelled_requests: MapSet.new()}
iex> new_state = RequestTracker.complete_request("req-123", state)
iex> Map.has_key?(new_state.pending_requests, "req-123")
false
@spec get_pending_request(request_id(), state()) :: {:ok, from()} | :error
Gets the from reference for a pending request.
Returns {:ok, from} if found, :error if not found.
Examples
iex> from = {self(), :tag}
iex> state = %{pending_requests: %{"req-123" => from}}
iex> RequestTracker.get_pending_request("req-123", state)
{:ok, from}
iex> RequestTracker.get_pending_request("req-456", state)
:error
@spec get_pending_request_ids(state()) :: [request_id()]
Gets all pending request IDs.
Examples
iex> state = %{pending_requests: %{"req-123" => {self(), :tag}, "req-456" => {self(), :tag2}}}
iex> RequestTracker.get_pending_request_ids(state) |> Enum.sort()
["req-123", "req-456"]
@spec handle_cancellation(request_id(), state()) :: {:reply, from(), state()} | {:noreply, state()}
Handles a cancellation notification.
If the request is still pending:
- Marks it as cancelled
- Removes it from pending
- Returns {:reply, from} to send cancellation reply
If the request is not pending:
- Just marks it as cancelled
- Returns {:noreply, state}
Examples
iex> from = {self(), :tag}
iex> state = RequestTracker.init() |> RequestTracker.track_request("req-123", from)
iex> {action, data, new_state} = RequestTracker.handle_cancellation("req-123", state)
iex> action
:reply
iex> data
from
iex> RequestTracker.cancelled?("req-123", new_state)
true
@spec init() :: map()
Initializes request tracker state.
Returns a map with empty pending_requests and cancelled_requests.
Examples
iex> RequestTracker.init()
%{pending_requests: %{}, cancelled_requests: MapSet.new()}
Merges request tracker state into existing state map.
Examples
iex> RequestTracker.init_state(%{foo: "bar"})
%{foo: "bar", pending_requests: %{}, cancelled_requests: MapSet.new()}
@spec process_if_not_cancelled(request_id(), from(), state()) :: {:ok, state()} | {:cancelled, state()}
Processes a request with cancellation checking.
This is a convenience function that:
- Checks if the request is already cancelled
- If not, tracks it as pending
- Returns appropriate response
Examples
iex> state = RequestTracker.init() |> RequestTracker.cancel_request("req-123")
iex> RequestTracker.process_if_not_cancelled("req-123", {self(), :tag}, state)
{:cancelled, state}
iex> state = RequestTracker.init()
iex> {:ok, new_state} = RequestTracker.process_if_not_cancelled("req-456", {self(), :tag}, state)
iex> Map.has_key?(new_state.pending_requests, "req-456")
true
@spec track_request(request_id(), from(), state()) :: state()
Tracks a new pending request.
Adds the request_id and from reference to pending_requests.
Examples
iex> state = RequestTracker.init()
iex> new_state = RequestTracker.track_request("req-123", {self(), :tag}, state)
iex> Map.has_key?(new_state.pending_requests, "req-123")
true