Tools
Tools are one of the core primitives in MCP. They allow an LLM to take actions on the user’s behalf, such as performing calculations, interacting with external APIs, or managing local files.
In mcp-cpp-sdk, tools are defined using the add_tool method on the Server class. Each tool requires:
Name: A unique identifier for the tool.
Description: A human-readable explanation of what the tool does.
Input Schema: A JSON Schema defining the input parameters.
Handler: A C++ function or lambda that executes the tool logic.
Basic Tool Registration
The simplest way to register a tool is with a synchronous handler that takes
nlohmann::json and returns nlohmann::json. A returned object made only of
CallToolResult members (content, structuredContent, isError,
_meta) that decodes as one is sent as the result, as in the example below.
Any other value is turned into a CallToolResult: a string becomes the text
block, an object becomes structuredContent with its serialization as the
text block, and anything else is serialized into the text block. An object
whose only member is an empty content array is treated as data and wrapped.
#include <mcp/mcp.hpp>
nlohmann::json schema = {
{"type", "object"},
{"properties", {
{"a", {{"type", "number"}}},
{"b", {{"type", "number"}}}
}},
{"required", nlohmann::json::array({"a", "b"})}
};
server.add_tool("add", "Add two numbers", std::move(schema),
[](const nlohmann::json& args) -> nlohmann::json {
double a = args.at("a").get<double>();
double b = args.at("b").get<double>();
std::string text = "Result: " + std::to_string(a + b);
return {{"content", nlohmann::json::array(
{nlohmann::json{{"type", "text"}, {"text", std::move(text)}}})}};
});
Typed Handlers
For better type safety and cleaner code, you can use custom C++ structs for tool input and output. Any type that implements to_json and from_json (using NLOHMANN_DEFINE_TYPE_INTRUSIVE or manual implementation) can be used.
Using typed handlers provides several advantages:
Automatic Validation: The SDK automatically deserializes the incoming JSON into your struct, validating the presence and types of required fields.
Type Safety: Your handler logic works with native C++ types rather than dynamic JSON objects.
Self-Documentation: The input and output structures serve as clear documentation of the tool’s interface.
The following example demonstrates defining input and output structures for a calculator tool:
struct AddInput {
double a;
double b;
};
inline void to_json(nlohmann::json& j, const AddInput& v) {
j = nlohmann::json{{"a", v.a}, {"b", v.b}};
}
inline void from_json(const nlohmann::json& j, AddInput& v) {
j.at("a").get_to(v.a);
j.at("b").get_to(v.b);
}
struct AddOutput {
double result;
};
inline void to_json(nlohmann::json& j, const AddOutput& v) { j = nlohmann::json{{"result", v.result}}; }
inline void from_json(const nlohmann::json& j, AddOutput& v) { j.at("result").get_to(v.result); }
You then register the tool by providing these types as template arguments to add_tool:
// Tool: add — sync handler, no context
nlohmann::json add_schema = {
{"type", "object"},
{"properties", {{"a", {{"type", "number"}}}, {"b", {{"type", "number"}}}}},
{"required", nlohmann::json::array({"a", "b"})}};
server.add_tool<AddInput, AddOutput>(
"add", "Add two numbers", add_schema,
[](AddInput in) -> AddOutput { return AddOutput{.result = in.a + in.b}; });
When using typed handlers, the SDK uses the template parameters to automatically wrap your handler in a layer that performs the conversion between nlohmann::json and your custom types.
Async Handlers with Context
Many tools need to perform asynchronous work, such as network requests or long-running computations. mcp-cpp-sdk supports coroutine-based handlers returning mcp::Task<T>.
Additionally, handlers can accept a mcp::Context& parameter to access server-level features:
Logging: Send real-time logs back to the client.
Progress Reporting: Inform the client about long-running task status.
Cancellation: Check if the client has requested to abort the task.
The following example shows a tool that logs information and performs an asynchronous operation:
// Tool: log_and_upper — async handler, with context (demonstrates ctx.log_info)
nlohmann::json upper_schema = {{"type", "object"},
{"properties", {{"text", {{"type", "string"}}}}},
{"required", nlohmann::json::array({"text"})}};
server.add_tool<nlohmann::json, nlohmann::json>(
"log_and_upper", "Uppercase the input and log via context", upper_schema,
[](Context& ctx, nlohmann::json in) -> Task<nlohmann::json> {
auto text = in.at("text").get<std::string>();
co_await ctx.log_info("Uppercasing: " + text);
for (auto& ch : text) {
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
}
nlohmann::json out = {{"result", text}};
co_return out;
});
Progress and Cancellation
For tasks that take a significant amount of time, it is good practice to report progress and periodically check for cancellation requests.
[](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}};
});
// ========== TRANSPORT & RUN ==========
auto [client_transport, server_transport] =
mcp::create_memory_transport_pair(io_ctx.get_executor());
For more details on using the context, see Server Context.
Error Handling
There are two primary ways to handle errors in tool handlers:
Throwing Exceptions: A
std::exceptionthrown by any tool handler (synchronous, typed or asynchronous) is converted into a tool error result (isError: true) whose text is the exception message. An exception from middleware is not: it is answered as a JSON-RPC-32603error (see Middleware).Explicit Error Returns: A handler declared with
Out=mcp::CallToolResultcan return a complete result whoseisErrorfield istrue.add_raw_tool()provides the equivalent low-level JSON escape hatch with runtime validation.
The following example demonstrates both patterns:
// Tool 1: Throws std::runtime_error
nlohmann::json throw_error_schema = {{"type", "object"},
{"properties", {{"message", {{"type", "string"}}}}},
{"required", nlohmann::json::array({"message"})}};
server.add_tool("throw_error", "Throws a runtime error", throw_error_schema,
[](const nlohmann::json& args) -> nlohmann::json {
std::cout << "[Server] throw_error handler called\n";
std::string msg = args.at("message").get<std::string>();
throw std::runtime_error("Intentional error: " + msg);
});
// Tool 2: Returns error JSON explicitly
nlohmann::json return_error_schema = {{"type", "object"},
{"properties", {{"code", {{"type", "integer"}}}}},
{"required", nlohmann::json::array({"code"})}};
server.add_tool<nlohmann::json, mcp::CallToolResult>(
"return_error", "Returns error JSON", return_error_schema,
[](const nlohmann::json& args) -> mcp::CallToolResult {
std::cout << "[Server] return_error handler called\n";
int code = args.at("code").get<int>();
mcp::CallToolResult result;
mcp::TextContent content;
content.text = "Application error with code " + std::to_string(code);
result.content.emplace_back(std::move(content));
result.isError = true;
return result;
});
Use isError: true when the tool completed normally but needs to report an
application-level failure back to the client. Either way the client receives a
normal tools/call result and the session stays open. This is a critical
distinction:
Protocol Errors: Issues like malformed JSON, invalid method names, or disconnected transports cause JSON-RPC level errors.
Application Errors: Issues within your tool’s logic (e.g., “file not found”, “invalid input value”) should usually be returned as
isError: truetool results so the client can inspect them as normal tool output.
Tool Output Schema
In addition to the input schema, you can provide an optional outputSchema. This allows clients to understand the structure of the data returned by the tool, enabling better integration with typed systems or UI rendering.
When an outputSchema is provided, the SDK includes it in the tool definition sent to clients.
The schema is advertised as outputSchema in the tool’s metadata, enabling clients to understand
the structure of the tool’s output. A tool with an outputSchema must return
structuredContent (unless the result is an error); otherwise the call fails with a JSON-RPC
error. The SDK does not validate that content against the schema at runtime.
nlohmann::json output_schema = {
{"type", "object"},
{"properties", {
{"result", {{"type", "number"}}}
}}
};
server.add_tool<AddInput, AddOutput>(
"add_with_output_schema",
"Add two numbers with defined output structure",
input_schema,
output_schema,
[](AddInput in) -> AddOutput { return {in.a + in.b}; }
);
Structured Content
While simple tools can return raw JSON, more complex tools might want to return structured content blocks (text, images, or embedded resources). The mcp::CallToolResult structure provides a standard way to return multiple content blocks.
nlohmann::json success_schema = {{"type", "object"},
{"properties", {{"value", {{"type", "integer"}}}}},
{"required", nlohmann::json::array({"value"})}};
server.add_tool("success", "Returns successfully", success_schema,
[](const nlohmann::json& args) -> nlohmann::json {
std::cout << "[Server] success handler called\n";
int value = args.at("value").get<int>();
mcp::CallToolResult result;
mcp::TextContent content;
content.text =
"Successfully doubled the value: " + std::to_string(value * 2);
result.content.emplace_back(std::move(content));
return nlohmann::json(result);
});
See Also
Server Context for details on logging and progress.
Core Concepts for an overview of core concepts.