ExMCP.Protocol.RequestTracker (ex_mcp v0.9.0)

View Source

Manages 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

from()

@type from() :: GenServer.from()

request_id()

@type request_id() :: String.t() | integer()

state()

@type state() :: map()

Functions

cancel_request(request_id, state)

@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

cancelled?(request_id, state)

@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

complete_request(request_id, state)

@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

get_pending_request(request_id, state)

@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

get_pending_request_ids(state)

@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"]

handle_cancellation(request_id, state)

@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

init()

@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()}

init_state(state)

@spec init_state(map()) :: map()

Merges request tracker state into existing state map.

Examples

iex> RequestTracker.init_state(%{foo: "bar"})
%{foo: "bar", pending_requests: %{}, cancelled_requests: MapSet.new()}

process_if_not_cancelled(request_id, from, state)

@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:

  1. Checks if the request is already cancelled
  2. If not, tracks it as pending
  3. 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

track_request(request_id, from, state)

@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