Logging, Progress Tracking, and Cancellation

This guide covers logging, progress tracking, and cancellation in MCP servers. These features help you build responsive, observable tools that communicate their status to clients.

Logging

Send log messages to MCP clients to help users understand what your tools are doing. The MCP server can send log notifications that the client can display to users.

Basic Logging

Inject McpLog into your tool methods to send log messages:

import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Process data with logging")
    String processData(String input, McpLog log) {
        log.info("Starting data processing for input: {}", input); (1)

        // Do some work
        String result = input.toUpperCase();

        log.debug("Processing complete. Result length: {}", result.length()); (2)

        return result;
    }
}
1 Log informational messages that the client can display.
2 Debug messages are only sent if the client’s log level allows it.

Log Levels

MCP supports multiple log levels:

import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.McpLog.LogLevel;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Demonstrate log levels")
    String demonstrateLevels(McpLog log) {
        log.debug("Debug information");           (1)
        log.info("Informational message");        (2)
        log.error("An error occurred");           (3)

        // Send custom log levels
        log.send(LogLevel.WARNING, "This is a warning"); (4)
        log.send(LogLevel.CRITICAL, "Critical issue");

        return "Check logs for details";
    }
}
1 Debug-level logging for detailed diagnostics.
2 Informational messages about normal operations.
3 Error messages for problems.
4 Send messages with custom log levels.

Error Logging with Exceptions

Log exceptions with stack traces:

import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Process with error handling")
    String processWithErrorHandling(String input, McpLog log) {
        try {
            return performRiskyOperation(input);
        } catch (Exception e) {
            log.error(e, "Failed to process input: {}", input); (1)
            return "Processing failed: " + e.getMessage();
        }
    }

    private String performRiskyOperation(String input) throws Exception {
        // Simulated operation that might fail
        if (input.isEmpty()) {
            throw new IllegalArgumentException("Input cannot be empty");
        }
        return input.toUpperCase();
    }
}
1 Log the exception with context about what failed.

Logger Names

The MCP logger name is automatically derived from the method name. For a tool method myTool(), the logger name will be tool:myTool.

Checking Log Level

Check the current log level before expensive logging operations:

import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.McpLog.LogLevel;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Efficient logging")
    String efficientLogging(McpLog log) {
        if (log.level().compareTo(LogLevel.DEBUG) <= 0) { (1)
            // Only compute expensive debug info if debug is enabled
            String expensiveDebugInfo = computeExpensiveDebugInfo();
            log.debug("Debug info: {}", expensiveDebugInfo);
        }

        return "Processing complete";
    }

    private String computeExpensiveDebugInfo() {
        // Expensive computation
        return "Detailed diagnostic information";
    }
}
1 Check the log level to avoid expensive operations when debug is disabled.

Progress Tracking

Report progress for long-running operations so clients can show progress indicators to users. Progress tracking requires the client to include a progress token in the request.

Using ProgressNotification

For manual progress reporting, use ProgressNotification:

import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;

public class MyTools {

    @Tool(description = "Long-running operation with progress")
    Uni<String> longOperation(Progress progress) {
        if (progress.token().isEmpty()) { (1)
            return Uni.createFrom().item("Progress tracking not available");
        }

        return Uni.createFrom().item("ok")
            .onItem().invoke(() -> {
                for (int i = 1; i <= 10; i++) {
                    // Perform some work
                    processStep(i);

                    // Report progress
                    progress.notificationBuilder() (2)
                        .setProgress(i) (3)
                        .setTotal(10) (4)
                        .setMessage("Processing step " + i + " of 10") (5)
                        .build()
                        .sendAndForget(); (6)
                }
            });
    }

    private void processStep(int step) {
        // Simulate work
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
1 Check if the client provided a progress token.
2 Create a new progress notification.
3 Set the current progress value.
4 Set the expected total value.
5 Set a descriptive message for the user.
6 Send the notification without waiting for confirmation.

Using ProgressTracker

For automatic progress tracking, use ProgressTracker:

import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressTracker;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Process with automatic progress tracking")
    String processWithTracking(Progress progress) {
        if (progress.token().isEmpty()) {
            return "Progress tracking not available";
        }

        ProgressTracker tracker = progress.trackerBuilder() (1)
            .setTotal(100) (2)
            .setDefaultStep(10) (3)
            .setMessageBuilder(current -> "Processed " + current + " items") (4)
            .build();

        // Process items and advance progress
        for (int i = 0; i < 10; i++) {
            processItems(10);
            tracker.advanceAndForget(); (5)
        }

        return "Processing complete. Total: " + tracker.progress();
    }

    private void processItems(int count) {
        // Simulate processing
    }
}
1 Create a new progress tracker.
2 Set the expected total (100 items).
3 Set the default step size for advanceAndForget().
4 Provide a function to build messages from the current progress.
5 Advance by the default step and send notification.

Advanced Progress Tracking

Track progress with custom increments:

import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressTracker;
import io.quarkiverse.mcp.server.Tool;
import java.math.BigDecimal;

public class MyTools {

    @Tool(description = "Advanced progress tracking")
    String advancedProgress(Progress progress) {
        if (progress.token().isEmpty()) {
            return "Not available";
        }

        ProgressTracker tracker = progress.trackerBuilder()
            .setTotal(100.0)
            .setMessageBuilder(p -> "Progress: " + p + "%")
            .build();

        // Different sized steps
        tracker.advanceAndForget(25); (1)
        processLargeChunk();

        tracker.advanceAndForget(15.5); (2)
        processMediumChunk();

        tracker.advanceAndForget(BigDecimal.valueOf(59.5)); (3)
        processRemainingWork();

        return "Complete at " + tracker.progress() + "%";
    }

    private void processLargeChunk() { }
    private void processMediumChunk() { }
    private void processRemainingWork() { }
}
1 Advance by an integer value.
2 Advance by a decimal value.
3 Advance by a BigDecimal for precision.

Async Progress Tracking

For async operations, use the async API:

import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressTracker;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;

public class MyTools {

    @Tool(description = "Async progress tracking")
    Uni<String> asyncProgress(Progress progress) {
        if (progress.token().isEmpty()) {
            return Uni.createFrom().item("Not available");
        }

        ProgressTracker tracker = progress.trackerBuilder()
            .setTotal(5)
            .setDefaultStep(1)
            .setMessageBuilder(p -> "Step " + p + " complete")
            .build();

        return processStepAsync(1)
            .onItem().call(() -> tracker.advance(1)) (1)
            .chain(() -> processStepAsync(2))
            .onItem().call(() -> tracker.advance(1))
            .chain(() -> processStepAsync(3))
            .onItem().call(() -> tracker.advance(1))
            .map(v -> "All steps complete");
    }

    private Uni<Void> processStepAsync(int step) {
        return Uni.createFrom().voidItem();
    }
}
1 Use advance() instead of advanceAndForget() to wait for the notification to be sent.

Progress Without Total

Track progress without a known total:

import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Progress without total")
    String progressWithoutTotal(Progress progress) {
        if (progress.token().isEmpty()) {
            return "Not available";
        }

        int itemsProcessed = 0;
        while (hasMoreItems()) {
            processItem();
            itemsProcessed++;

            progress.notificationBuilder()
                .setProgress(itemsProcessed) (1)
                // No total set
                .setMessage("Processed " + itemsProcessed + " items")
                .build()
                .sendAndForget();
        }

        return "Processed " + itemsProcessed + " items total";
    }

    private boolean hasMoreItems() { return false; }
    private void processItem() { }
}
1 Report progress without setting a total when the total is unknown.

Cancellation

Handle request cancellation from clients, allowing users to stop long-running operations.

Basic Cancellation Handling

Check for cancellation during long-running operations:

import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Cancellation.Result;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Cancellable operation")
    String cancellableOperation(Cancellation cancellation) {
        for (int i = 0; i < 100; i++) {
            // Check if cancellation was requested
            Result result = cancellation.check(); (1)

            if (result.isRequested()) { (2)
                String reason = result.reason().orElse("No reason provided");
                return "Operation cancelled: " + reason;
            }

            // Perform work
            processItem(i);
        }

        return "Operation completed successfully";
    }

    private void processItem(int item) {
        // Simulate work
    }
}
1 Check if the client requested cancellation.
2 If cancellation was requested, stop processing and return.

Throwing OperationCancellationException

Use the exception-based approach for cleaner code:

import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Cancellation.OperationCancellationException;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Operation with cancellation exception")
    String operationWithException(Cancellation cancellation) {
        try {
            for (int i = 0; i < 100; i++) {
                cancellation.skipProcessingIfCancelled(); (1)
                processItem(i);
            }
            return "Complete";
        } catch (OperationCancellationException e) {
            // Clean up if needed
            return "Cancelled"; (2)
        }
    }

    private void processItem(int item) {
        // Simulate work
    }
}
1 Throws OperationCancellationException if cancellation was requested.
2 Handle the cancellation exception to perform cleanup.

Cancellation with Cleanup

Perform cleanup when an operation is cancelled:

import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Cancellation.OperationCancellationException;
import io.quarkiverse.mcp.server.Cancellation.Result;
import io.quarkiverse.mcp.server.Tool;
import java.util.ArrayList;
import java.util.List;

public class MyTools {

    @Tool(description = "Cancellable with cleanup")
    String cancellableWithCleanup(Cancellation cancellation) {
        List<Resource> resources = new ArrayList<>();

        try {
            for (int i = 0; i < 100; i++) {
                Result result = cancellation.check();
                if (result.isRequested()) {
                    throw new OperationCancellationException();
                }

                Resource resource = acquireResource(i);
                resources.add(resource);
                processResource(resource);
            }

            return "All resources processed";

        } catch (OperationCancellationException e) {
            // Clean up acquired resources
            resources.forEach(this::releaseResource); (1)
            return "Cancelled - cleaned up " + resources.size() + " resources";
        }
    }

    private Resource acquireResource(int id) { return new Resource(id); }
    private void processResource(Resource r) { }
    private void releaseResource(Resource r) { }

    record Resource(int id) { }
}
1 Clean up resources when the operation is cancelled.

Checking Cancellation Reason

Access the cancellation reason if provided:

import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Cancellation.Result;
import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Check cancellation reason")
    String checkCancellationReason(Cancellation cancellation, McpLog log) {
        for (int i = 0; i < 100; i++) {
            Result result = cancellation.check();

            if (result.isRequested()) {
                if (result.reason().isPresent()) { (1)
                    String reason = result.reason().get();
                    log.info("Cancelled due to: {}", reason);
                    return "Cancelled: " + reason;
                } else {
                    return "Cancelled (no reason provided)";
                }
            }

            processItem(i);
        }

        return "Complete";
    }

    private void processItem(int item) { }
}
1 Check if a cancellation reason was provided by the client.

Combining Features

Combine logging, progress, and cancellation for robust long-running operations:

import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Cancellation.OperationCancellationException;
import io.quarkiverse.mcp.server.McpLog;
import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressTracker;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Robust long-running operation")
    String robustOperation(McpLog log, Progress progress, Cancellation cancellation) {
        log.info("Starting operation");

        ProgressTracker tracker = null;
        if (progress.token().isPresent()) { (1)
            tracker = progress.trackerBuilder()
                .setTotal(100)
                .setDefaultStep(1)
                .setMessageBuilder(p -> "Processing: " + p + "%")
                .build();
        }

        try {
            for (int i = 0; i < 100; i++) {
                // Check for cancellation
                cancellation.skipProcessingIfCancelled(); (2)

                // Perform work
                processItem(i);

                // Update progress
                if (tracker != null) {
                    tracker.advanceAndForget(); (3)
                }

                // Log every 25%
                if (i % 25 == 0) {
                    log.info("Progress: {}%", i); (4)
                }
            }

            log.info("Operation completed successfully");
            return "Success";

        } catch (OperationCancellationException e) {
            log.info("Operation cancelled");
            return "Cancelled";
        } catch (Exception e) {
            log.error(e, "Operation failed"); (5)
            return "Failed: " + e.getMessage();
        }
    }

    private void processItem(int item) { }
}
1 Set up progress tracking if the client provided a token.
2 Check for cancellation at each step.
3 Report progress updates.
4 Log milestones.
5 Log errors with full context.