Error Handling ============== This guide covers error handling patterns in the MCP C++ SDK, focusing on both server-side and client-side strategies. Proper error handling ensures that your MCP applications are resilient, informative, and provide a smooth user experience even when things go wrong. Overview -------- The MCP C++ SDK leverages a combination of JSON-RPC error codes, C++ exceptions, and structured result flags to communicate errors between servers and clients. The primary goals of the error handling system are: 1. **Clarity**: Provide meaningful error messages and codes to identify the source of the problem. 2. **Isolation**: Prevent a single failure from crashing the entire server or client session. 3. **Graceful Degradation**: Allow clients to handle errors and continue functioning where possible. JSON-RPC Error Codes -------------------- The SDK follows the JSON-RPC 2.0 specification for error reporting. Standard error codes include: - ``-32700``: Parse error (Invalid JSON received) - ``-32600``: Invalid Request - ``-32601``: Method not found - ``-32602``: Invalid params - ``-32603``: Internal error Additionally, the SDK defines MCP-specific codes used throughout the library: - ``-32000``: Unauthorized - ``-32001``: Request timed out - ``-32800``: Request cancelled When implementing custom tools, you should prefer using standard error codes where they apply, or use codes in the range ``-32000`` to ``-32099`` for application-specific protocol errors. Server-side Error Handling -------------------------- Server handlers (tools, resources, prompts) can report errors in two primary ways: 1. **Throwing Exceptions**: Exceptions from a tool handler — typed, asynchronous or raw — are converted into tool results with ``isError: true``, because a tool that cannot complete has failed at its own task rather than at the protocol. Exceptions from resource and prompt handlers, which have no such result channel, propagate as JSON-RPC ``Internal error (-32603)`` responses, and so do exceptions from middleware: middleware decides whether a call may proceed at all, so refusing one is a protocol-level answer rather than a tool outcome. Exception messages are flattened and length-bounded before they reach the peer. 2. **Returning Error Results**: For application-level errors (e.g., "File not found" or "Invalid input"), handlers can return a ``CallToolResult`` with the ``isError`` flag set to ``true``. This allows the client to distinguish between a technical failure (like a crash or timeout) and a logical error within the tool's execution. Choosing Between Exceptions and Error Results ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Use **Exceptions** for: - Unexpected system failures (e.g., memory allocation failure, disk I/O errors). - Programming errors (e.g., out-of-bounds access, logic errors). - Conditions that prevent the handler from fulfilling its contract at all. - Use **Error Results** for: - Expected failure modes (e.g., user not found, validation errors). - Situations where the tool successfully ran but couldn't produce the desired output. - Providing detailed, structured error information back to the LLM. Example: Throwing an Exception ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../../examples/features/error_handling.cpp :language: cpp :start-after: // Tool 1: Throws std::runtime_error :end-before: // Tool 2: Returns error JSON explicitly Example: Returning an Error Result ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../../examples/features/error_handling.cpp :language: cpp :start-after: // Tool 2: Returns error JSON explicitly :end-before: // Tool 3: Accesses invalid argument (nlohmann::json throws) Implicit Exceptions ~~~~~~~~~~~~~~~~~~~ It's important to be aware that some library calls might throw exceptions that you don't explicitly catch. For instance, ``nlohmann::json`` throws an exception if you try to access a non-existent key using ``at()``. In a tool handler such an exception becomes a tool error result like any other; in a resource or prompt handler it becomes a JSON-RPC internal error. .. literalinclude:: ../../examples/features/error_handling.cpp :language: cpp :start-after: // Tool 3: Accesses invalid argument (nlohmann::json throws) :end-before: // Tool 4: Successful tool (for comparison) Client-side Error Handling -------------------------- Clients should always wrap server calls in ``try-catch`` blocks to handle potential network issues, protocol violations, or server-side errors. .. literalinclude:: ../../examples/features/error_handling.cpp :language: cpp :start-after: // ========== TEST 1: Catch exception from thrown error ========== :end-before: // Wait between tests Request Timeouts ~~~~~~~~~~~~~~~~ .. important:: **Every client request carries a 30-second deadline by default.** Nothing opts into it: ``ClientOptions::request_timeout`` starts at ``std::chrono::seconds(30)``, and a default-constructed ``Client`` uses it for ``call_tool``, ``read_resource``, ``list_tools`` and every other request. A tool that legitimately runs longer than that — a large model call, a slow build, a batch job — fails on the client side while the server is still working on it. Raise it for the whole client by passing ``ClientOptions`` to the constructor: .. code-block:: cpp mcp::ClientOptions options; options.request_timeout = std::chrono::minutes(5); mcp::Client client(transport, io.get_executor(), options); A single request can be given its own deadline through ``RequestOptions``, but only on the untyped ``send_request`` overload. The typed helpers — ``call_tool``, ``read_resource``, ``list_tools`` and the rest — take no per-request options and always use the client-wide default, so a client that makes one slow call among many fast ones either raises the default for all of them or drops to ``send_request`` for that one: .. code-block:: cpp mcp::CallToolParams call_params; call_params.name = "render"; call_params.arguments = args; mcp::RequestOptions slow; slow.timeout = std::chrono::minutes(30); auto raw = co_await client.send_request( "tools/call", nlohmann::json(call_params), slow); auto result = raw.get(); When the deadline expires the awaiting coroutine throws :cpp:class:`mcp::McpError` with code ``-32001`` (:cpp:var:`mcp::g_REQUEST_TIMEOUT`) and the message ``Request timed out``. Catch it the same way as any other ``McpError``. .. warning:: A timeout is a local decision, not a cancellation. A request whose bytes have not been written yet is dropped before transmission, but once transport I/O has started the server may still run the call to completion after the client has given up. Treat a timed-out non-idempotent request as having an unknown outcome, and see the retry guidance below before repeating it. A non-positive timeout is rejected rather than treated as "no deadline": constructing a ``Client`` with one, or passing one in ``RequestOptions``, throws ``std::invalid_argument``. The SDK offers no way to disable the deadline entirely — use a duration long enough for the slowest call you expect. Catching Server Errors ~~~~~~~~~~~~~~~~~~~~~~ When a server returns a JSON-RPC error response, the ``Client`` methods (like ``call_tool`` or ``read_resource``) throw ``mcp::McpError``, a ``std::runtime_error`` that carries the error code and message. This simplifies the client logic, as you can use standard C++ exception handling for both local and remote failures. Handling Application Errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the server returns a successful JSON-RPC response but the tool itself failed (indicated by ``isError: true``), the client receives the result normally and must check the flag. This is common when the tool executes correctly but encounters a business logic error that the LLM should be aware of. .. literalinclude:: ../../examples/features/error_handling.cpp :language: cpp :start-after: // ========== TEST 2: Handle error JSON response ========== :end-before: // Wait between tests Graceful Degradation Patterns ----------------------------- To ensure your application remains usable during partial failures, consider the following patterns: - **Feature Toggling**: If a specific tool call fails consistently, your client application can temporarily disable or hide the functionality associated with that tool. - **Fallback Content**: When reading a resource fails, provide sensible default content or previously cached values. This is particularly useful for configuration or preference resources. - **UI Feedback**: Instead of showing a generic "Server Error," use the detailed error message from the server to inform the user about what went wrong (e.g., "The search service is currently down, please try again later"). - **Partial Results**: In operations that return lists or multiple pieces of data, consider returning the successfully retrieved parts even if some encountered errors. Retry Strategies ---------------- For transient errors, such as network timeouts or temporary server busy states (indicated by specific error codes), implementing a retry strategy is highly beneficial. - **Exponential Backoff**: Increase the wait time between retries (e.g., 100ms, 200ms, 400ms) to avoid overwhelming a struggling server. - **Max Retries**: Always define a maximum number of attempts to avoid infinite loops. - **Idempotency**: Only retry operations that are idempotent (safe to repeat), such as reading resources or pings. Avoid retrying tools that perform side effects (like making a purchase or deleting a file) unless you are certain it's safe. Logging and Monitoring ---------------------- The SDK provides a built-in logging system that is essential for debugging and monitoring production systems. Use the ``Context`` object in your handlers to log errors and warnings. .. code-block:: cpp server.add_tool("example", "...", schema, [](mcp::Context& ctx, nlohmann::json args) -> mcp::Task { try { // ... work ... } catch (const std::exception& e) { co_await ctx.log(mcp::LoggingLevel::eError, "Tool failed: " + std::string(e.what())); throw; } }); By logging at the server level, you can aggregate errors across multiple clients and identify systemic issues. For more details on logging, see the :doc:`../concepts/context` guide. Example Walkthrough ------------------- The ``examples/features/error_handling.cpp`` file provides a complete, runnable example of these patterns. It demonstrates: 1. A server with multiple tools that fail in different ways. 2. A client that catches these failures and continues to function. 3. Successful recovery where a subsequent tool call succeeds after previous failures. Run the example to see the output: .. code-block:: bash ./build/release/example-feature-error-handling