Progress Tracking
Hermes MCP supports progress notifications for long-running operations as specified in the MCP protocol.
Overview
The MCP specification includes a progress notification mechanism that allows communicating the progress of long-running operations. Progress updates are useful for:
- Providing feedback for long-running server operations
- Updating users about the status of operations that might take time to complete
- Enabling client applications to display progress indicators for better UX
Progress Tokens
Progress is tracked using tokens that uniquely identify a specific operation. Hermes provides a helper function to generate unique tokens:
# Generate a unique progress token
progress_token = Hermes.Message.generate_progress_token()
Making Requests with Progress Tracking
Any request can include a progress token to indicate you want to track progress:
# Make a request with progress tracking
Hermes.Client.read_resource(client, "resource-uri",
progress: [token: progress_token]
)
Receiving Progress Updates
Method 1: Register a Callback Function
Before making the request, register a callback function to be called when progress notifications arrive:
# Register a callback for a specific progress token
Hermes.Client.register_progress_callback(client, progress_token,
fn token, progress, total ->
percentage = if total, do: progress / total * 100, else: nil
IO.puts("Operation #{token} progress: #{progress}/#{total || "unknown"} (#{percentage || "unknown"}%)")
end
)
Method 2: Provide a Callback with the Request
You can combine the token and callback in a single request:
# Combined approach: request with progress token and callback
Hermes.Client.list_tools(client,
progress: [
token: progress_token,
callback: fn token, progress, total ->
# Handle progress updates
IO.puts("Progress: #{progress}/#{total || "unknown"}")
end
]
)
Sending Progress Updates (Server Implementation)
When implementing server-side functionality, you can send progress updates to clients:
# Send a progress update to clients
Hermes.Client.send_progress(client, progress_token, 50, 100)
Cleanup
To avoid memory leaks, unregister callbacks when you're no longer interested in progress updates:
# Unregister a progress callback
Hermes.Client.unregister_progress_callback(client, progress_token)
Complete Example
defmodule MyApp.LongRunningOperation do
def execute_with_progress(client) do
# Generate a unique token
progress_token = Hermes.Message.generate_progress_token()
# Register a callback to handle progress updates
Hermes.Client.register_progress_callback(client, progress_token, fn _token, progress, total ->
if total do
percentage = Float.round(progress / total * 100, 1)
IO.puts("Progress: #{progress}/#{total} (#{percentage}%)")
else
IO.puts("Progress: #{progress}/unknown")
end
end)
# Make the request with the progress token
result = Hermes.Client.call_tool(client, "long-running-operation", %{},
progress: [token: progress_token]
)
# Cleanup by unregistering the callback
Hermes.Client.unregister_progress_callback(client, progress_token)
# Return the result
result
end
end
Using Progress within Phoenix LiveView
Progress tracking is particularly useful in interactive web applications:
defmodule MyAppWeb.ProcessLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, progress: 0, total: 100, running: false)}
end
def handle_event("start_process", _params, socket) do
token = Hermes.Message.generate_progress_token()
# Register progress callback
Hermes.Client.register_progress_callback(MyApp.Client, token, fn _token, progress, total ->
send(self(), {:progress_update, progress, total})
end)
# Start the operation asynchronously
Task.start(fn ->
result = Hermes.Client.call_tool(MyApp.Client, "long-running-operation", %{},
progress: [token: token]
)
Hermes.Client.unregister_progress_callback(MyApp.Client, token)
send(self(), {:operation_complete, result})
end)
{:noreply, assign(socket, running: true)}
end
def handle_info({:progress_update, progress, total}, socket) do
{:noreply, assign(socket, progress: progress, total: total || 100)}
end
def handle_info({:operation_complete, result}, socket) do
{:noreply, assign(socket, running: false, result: result)}
end
def render(assigns) do
~H"""
<div>
<button :if={!@running} phx-click="start_process">Start Process</button>
<div :if={@running} class="progress-bar">
<div class="progress" style={"width: #{@progress / @total * 100}%"}></div>
<span><%= @progress %>/<%= @total %></span>
</div>
<div :if={@result}>
<h3>Result:</h3>
<pre><%= inspect(@result) %></pre>
</div>
</div>
"""
end
end
This feature enables applications to provide rich, interactive feedback during long-running operations, enhancing the user experience.