Server Context

The mcp::Context object is a fundamental construct in the MCP C++ SDK. It is passed as a reference to every asynchronous handler for tools, resources, and prompts. Think of the Context as the per-request interface between your server’s logic and the connected client.

Core Responsibilities

The Context object handles three main categories of interaction:

  1. Downstream Communication: Sending real-time updates to the client (logging and progress).

  2. Request Lifecycle: Managing state specific to the current request (cancellation).

  3. Reverse RPC: Initiating requests from the server back to the client (sampling, elicitation, and roots).

Lifecycle and Safety

A Context is valid only for the duration of the request handler. It should never be stored beyond the co_return of the handler. The SDK ensures that the underlying transport and state required by the Context remain valid while the handler’s coroutine is active.

Note

The Context is designed to be coroutine-safe. You can safely pass it by reference into nested coroutines or helper functions, provided they are awaited within the main handler.

Logging

You can send log messages to the client using various log levels. The SDK respects the server’s current log level, automatically filtering messages that are less severe than the configured threshold.

Available log levels (from most to least severe):

  • Emergency (LoggingLevel::eEmergency): System is unusable.

  • Alert (LoggingLevel::eAlert): Action must be taken immediately.

  • Critical (LoggingLevel::eCritical): Critical conditions.

  • Error (LoggingLevel::eError): Error conditions.

  • Warning (LoggingLevel::eWarning): Warning conditions.

  • Notice (LoggingLevel::eNotice): Normal but significant conditions.

  • Info (LoggingLevel::eInfo): Informational messages.

  • Debug (LoggingLevel::eDebug): Debug-level messages.

#include <mcp/mcp.hpp>

server.add_tool<nlohmann::json, nlohmann::json>("debug_tool", "Tool with logging", schema,
                [](mcp::Context& ctx, nlohmann::json args) -> mcp::Task<nlohmann::json> {
                    // Direct level helpers
                    co_await ctx.log_info("Executing debug tool...");

                    // Generic log method
                    co_await ctx.log(mcp::LoggingLevel::eDebug, "Parameters: " + args.dump());

                    if (args.contains("error")) {
                        co_await ctx.log(mcp::LoggingLevel::eError,
                                         "An intentional error occurred");
                    }

                    co_return nlohmann::json{{"status", "ok"}};
                });

Logging is an asynchronous operation but generally returns quickly as it just pushes to the transport buffer. It’s a non-blocking way to provide visibility into server-side operations without affecting the final tool output.

Progress Reporting

For long-running tasks, you can report progress to the client. This is essential for providing feedback during operations like heavy computation, large file transfers, or multi-step API interactions.

Prerequisites

Progress reporting requires: 1. The client must support the notifications/progress capability. 2. The client must provide a progressToken in the request metadata (_meta).

If no token is provided in the request, ctx.report_progress() calls are silently ignored.

Usage Details

The report_progress method accepts: * progress: The current progress value (double). * total: An optional total progress value (double). * message: An optional human-readable progress message string.

nlohmann::json tool_schema = {{"type", "object"},
                              {"properties", {{"duration_ms", {{"type", "integer"}}}}},
                              {"required", nlohmann::json::array({"duration_ms"})}};

server.add_tool<nlohmann::json, nlohmann::json>(
    "long_task", "A long-running task that reports progress and can be cancelled", tool_schema,
    [](Context& ctx, const nlohmann::json& args) -> Task<nlohmann::json> {
        int duration_ms = args.at("duration_ms").get<int>();
        int steps = 10;
        int step_duration_ms = duration_ms / steps;

        std::cout << "Tool: Starting long_task for " << duration_ms << "ms\n";

        for (int i = 0; i < steps; ++i) {
            if (ctx.is_cancelled()) {
                std::cout << "Tool: Cancellation detected at step " << i << "\n";
                co_return nlohmann::json{
                    {"content", nlohmann::json::array({nlohmann::json{
                                    {"type", "text"},
                                    {"text", "Task cancelled at step " + std::to_string(i) +
                                                 "/" + std::to_string(steps)}}})},
                    {"isError", true}};
            }

            auto executor = co_await asio::this_coro::executor;
            asio::steady_timer work_timer(executor);
            work_timer.expires_after(std::chrono::milliseconds(step_duration_ms));
            co_await work_timer.async_wait(asio::use_awaitable);

            int progress = (i + 1) * 100 / steps;
            std::string message =
                "Processing step " + std::to_string(i + 1) + "/" + std::to_string(steps);
            co_await ctx.report_progress(progress, 100, message);
            std::cout << "Tool: Reported progress " << progress << "%\n";
        }

        std::cout << "Tool: Completed all steps\n";
        co_return nlohmann::json{
            {"content", nlohmann::json::array(
                            {nlohmann::json{{"type", "text"},
                                            {"text", "Task completed successfully with " +
                                                         std::to_string(steps) + " steps"}}})},
            {"isError", false}};
    });

The example above demonstrates a typical pattern where a tool loops through several steps, reporting the percentage completion and a descriptive message at each stage.

Cancellation

MCP supports cooperative cancellation, allowing clients to stop execution of requests that are no longer needed. This is particularly important for expensive LLM operations or long-running background tasks.

How it works

  1. A client sends a notifications/cancelled message for a specific request ID.

  2. The server marks the corresponding Context as cancelled.

  3. The tool handler checks ctx.is_cancelled() and exits gracefully.

Implementation Pattern

Handlers should periodically check ctx.is_cancelled() during long-running operations. Since cancellation is cooperative, the server doesn’t forcibly terminate the coroutine; it’s the handler’s responsibility to stop work.

for (int i = 0; i < steps; ++i) {
    // Check for cancellation at the start of each iteration
    if (ctx.is_cancelled()) {
        co_await ctx.log_info("Task cancelled by client. Cleaning up...");
        // Perform any necessary cleanup (closing files, etc.)
        co_return nlohmann::json{
            {"content", {{{"type", "text"}, {"text", "Task cancelled"}}}},
            {"isError", true}
        };
    }

    // ... perform expensive work ...
    co_await ctx.report_progress(i + 1, steps, "Step " + std::to_string(i+1));
}

Sampling (Reverse RPC)

Sampling allows the server to request a completion from the LLM via the client. This “Reverse RPC” capability enables servers to build complex, multi-step workflows where the server uses the client’s LLM to process data or make decisions.

Usage Example

The server sends a sampling/createMessage request back to the client. The client (e.g., Claude Desktop) then handles the request, often by prompting the user or using its own internal model.

server.add_tool<nlohmann::json, nlohmann::json>("generate_summary", "Requests text from LLM", schema,
                [](mcp::Context& ctx, nlohmann::json args) -> mcp::Task<nlohmann::json> {
                    mcp::SamplingMessage msg;
                    msg.role = mcp::Role::eUser;
                    msg.content = mcp::SamplingMessageContentBlock{
                        mcp::TextContent{.text = "Summarize this: " + args["text"].get<std::string>()}};

                    mcp::CreateMessageRequestParams params;
                    params.messages.push_back(std::move(msg));
                    params.maxTokens = 100;

                    try {
                        auto result = co_await ctx.sample_llm(params);
                        std::string summary;
                        if (auto* block = std::get_if<mcp::SamplingMessageContentBlock>(&result.content)) {
                            if (auto* tc = std::get_if<mcp::TextContent>(block)) {
                                summary = tc->text;
                            }
                        }
                        co_return nlohmann::json{
                            {"content", nlohmann::json::array(
                                {nlohmann::json{{"type", "text"}, {"text", summary}}})}};
                    } catch (const std::exception& e) {
                        co_await ctx.log(mcp::LoggingLevel::eError,
                                         "Sampling failed: " + std::string(e.what()));
                        co_return nlohmann::json{
                            {"content", nlohmann::json::array(
                                {nlohmann::json{{"type", "text"}, {"text", "Sampling failed"}}})}};
                    }
                });

Elicitation

Elicitation enables the server to request additional information or confirmation from the user. It’s often used when a tool needs parameters that weren’t initially provided or when a sensitive action requires explicit user approval.

Modes of Elicitation

  1. Form Mode: Requests the user to fill out a structured form defined by a JSON schema.

  2. URL Mode: Directs the user to an external URL (e.g., for OAuth flows).

nlohmann::json tool_schema = {{"type", "object"},
                              {"properties", {{"task", {{"type", "string"}}}}},
                              {"required", nlohmann::json::array({"task"})}};

server.add_tool<nlohmann::json, nlohmann::json>(
    "request_user_info", "A tool that elicits user information via form-based elicitation",
    tool_schema, [](Context& ctx, const nlohmann::json& args) -> Task<nlohmann::json> {
        std::string task = args.at("task").get<std::string>();
        std::cout << "Tool: Starting request_user_info for task: " << task << "\n";

        ElicitRequestFormParams form_params;
        form_params.mode = "form";
        form_params.message = "Please provide your contact information";
        form_params.requestedSchema = nlohmann::json{
            {"type", "object"},
            {"properties",
             nlohmann::json{
                 {"name", nlohmann::json{{"type", "string"}, {"title", "Full Name"}}},
                 {"email", nlohmann::json{{"type", "string"}, {"title", "Email Address"}}},
                 {"priority",
                  nlohmann::json{{"type", "string"},
                                 {"title", "Priority Level"},
                                 {"enum", nlohmann::json::array({"low", "medium", "high"})}}}}},
            {"required", nlohmann::json::array({"name", "email"})}};

        std::cout << "Tool: Sending elicitation request\n";
        ElicitRequestParams params = form_params;
        auto result = co_await ctx.elicit(params);

        std::cout << "Tool: Received elicitation result\n";

        auto make_result = [](const std::string& text) {
            return nlohmann::json{
                {"content",
                 nlohmann::json::array(
                     {nlohmann::json{{"type", "text"}, {"text", text}}})}};
        };

        if (result.action == ElicitAction::eAccept) {
            std::cout << "Tool: User accepted the form\n";
            if (result.content) {
                std::cout << "Tool: Form data: " << result.content->dump() << "\n";
                co_return make_result("Accepted: task=" + task +
                                     " info=" + result.content->dump());
            }
            co_return make_result("Accepted: task=" + task + " (no form data)");
        } else if (result.action == ElicitAction::eDecline) {
            std::cout << "Tool: User declined the form\n";
            co_return make_result("Declined: task=" + task);
        } else {
            std::cout << "Tool: User cancelled the form\n";
            co_return make_result("Cancelled: task=" + task);
        }
    });

The elicitation result contains an action (accept, decline, or cancel) and the elicited content (form data or response).

Roots

Roots discovery allows a server to discover the files and directories the client has made available. This is crucial for servers that need to operate on local files while maintaining a security sandbox defined by the client.

The request_roots() method returns a list of roots that the client has explicitly shared. This might include: * Local project directories. * Specific documentation folders. * In-memory virtual file systems.

nlohmann::json tool_schema = {
    {"type", "object"}, {"properties", {}}, {"required", nlohmann::json::array()}};

server.add_tool<nlohmann::json, nlohmann::json>(
    "discover_roots", "Discovers and displays the client's declared root URIs", tool_schema,
    [](Context& ctx, const nlohmann::json&) -> Task<nlohmann::json> {
        std::cout << "Tool: Requesting roots from client\n";

        try {
            auto roots_result = co_await ctx.request_roots();

            std::cout << "Tool: Received " << roots_result.roots.size() << " root(s)\n";

            std::string output = "Discovered roots:\n";
            for (const auto& root : roots_result.roots) {
                std::cout << "Tool:   - URI: " << root.uri;
                if (root.name) {
                    std::cout << " (name: " << *root.name << ")";
                }
                std::cout << "\n";

                output += "  - " + root.uri;
                if (root.name) {
                    output += " (" + *root.name + ")";
                }
                output += "\n";
            }

            co_return nlohmann::json{
                {"content",
                 nlohmann::json::array({nlohmann::json{{"type", "text"}, {"text", output}}})},
                {"isError", false}};
        } catch (const std::exception& e) {
            std::cout << "Tool: Error requesting roots: " << e.what() << "\n";
            std::string error_msg = "Error: " + std::string(e.what());
            co_return nlohmann::json{{"content", nlohmann::json::array({nlohmann::json{
                                                     {"type", "text"}, {"text", error_msg}}})},
                                     {"isError", true}};
        }
    });

The server calls ctx.request_roots() to get a list of Root objects, each containing a URI and an optional name. This is particularly useful for tools like “file_search” or “code_indexer” that need to know where they are allowed to look.

Advanced: Context Internal Architecture

While most developers will only use the public API of Context, understanding its internal structure can help when debugging or extending the SDK.

The Context holds: * A reference to the ITransport for sending notifications. * A RequestSender (a functional wrapper) for performing reverse RPC. * A shared atomic flag for cancellation. * A ProgressToken extracted from the request’s metadata.

This architecture ensures that Context is lightweight and efficient to pass around, while providing a rich set of capabilities.

Best Practices

  • Check Cancellation Often: For any operation taking more than a few hundred milliseconds, check is_cancelled() regularly.

  • Meaningful Progress Messages: Don’t just send percentages; send messages like “Scanning files…”, “Parsing AST…”, or “Uploading results…”.

  • Log Level Discipline: Keep Info and above for significant events. Use Debug for the “firehose” of internal state.

  • Handle Reverse RPC Failures: Sampling and Elicitation can fail due to network issues, client rejection, or user cancellation. Always use try-catch blocks or check result statuses.

  • Feature Detection: Remember that some clients might not support all features (like sampling or roots). The SDK handles the protocol level, but your tool logic should be resilient to missing capabilities.

Cross-References

To further explore the MCP C++ SDK and how it integrates with other features, consider these resources:

  • Tools: Comprehensive guide to tool registration and schemas.

  • Resources: Learn how to expose read-only data via MCP URIs.

  • Prompts: Guide to creating dynamic prompt templates for LLMs.

  • Transports: Understand the underlying communication layers (Stdio, WebSocket, HTTP).

  • Elicitation (if available): Advanced patterns for user interaction.

  • Roots (if available): Deep dive into the roots discovery protocol.

  • Examples: The examples/features/ directory in the source tree contains self-contained code for all features discussed here.

Summary

The mcp::Context is the bridge between your tool logic and the external world. By effectively using logging, progress reporting, and reverse RPC, you can create highly interactive and intelligent MCP servers that provide a superior user experience.