MCP Server¶
In this chapter, we will:
Initialize the STDIO server (if STDIO transport is preferred)
Initialize the HTTP server (if HTTP transport is preferred)
- Implement tools such as:
add_int64: adds two 64 bit integers.
add_float64: adds two 64 bit floats.
echo: will echo back the received string argument to the client.
- Expose resources such as:
file:///content.txt: Contains random text.
- Server prompts such as:
greeting_prompt: Will return a greetings prompt template with the argument substituted inside.
mbase_sdk_inform == Will return a prompt in the format: “Tell me about MBASE {argument}” in which the argument is provided by the client. We will implement a prompt compilation with this one.
Register those features
Implement the program loop.
STDIO Server Initialization¶
In order to initialize the STDIO server, include the following header:
#include <mbase/mcp/mcp_server_stdio.h>
Then, you will instantiate the McpServerStdio
class as follows:
#include <mbase/mcp/mcp_server_stdio.h>
int main()
{
mbase::McpServerStdio mcpServer(
"MCP Sample Server",
"1.0.0"
);
}
After the instantiation is complete, we will start the stdio transport thread as follows:
mcpServer.start_processor(); // doesn't block the main thread
Which will make the server read stdin in parallel thread waiting for parent process to send messages.
Since it runs on parallel, we will need to synchronize with the transport thread at some point in the program which will be mentioned in the Implementing the Program Loop.
HTTP Server Initialization¶
In order to initialize the stateless HTTP server, include the following header:
#include <mbase/mcp/mcp_server_http_streamable.h>
Then, you will instantiate the mbase::McpServerHttpStreamableStateless
class as follows:
#include <mbase/mcp/mcp_server_http_streamable.h>
int main()
{
mbase::McpServerHttpStreamableStateless mcpServer(
"MCP Sample Server",
"1.0.0",
"127.0.0.1", // hostname
8080 // port
);
}
After the instantiation is complete, we will start the HTTP transport thread as follows:
mcpServer.start_processor(); // doesn't block the main thread
Which will make the server accept HTTP requests in parallel thread.
Since it runs on parallel, we will need to synchronize with the transport thread at some point in the program which will be mentioned in the Implementing the Program Loop.
User can also disable the HTTP server by calling:
mcpServer.stop_processor();
Implementing Features¶
Feature registration workflow is as follows:
Include the
mcp_server_features.h
file.Implement the feature callback and respond with a valid response object defined under the
mcp_server_responses.h
.Initialize argument description objects if the features requires arguments.
Initialize the feature description object.
Register the feature description object to the server.
Start by including the mcp_server_features.h
and mcp_server_responses.h
file.
#include <mbase/mcp/mcp_server_stdio.h>
#include <mbase/mcp/mcp_server_features.h> // hi!
#include <mbase/mcp/mcp_server_responses.h> // hi!
int main()
{
mbase::McpServerStdio mcpServer(
"MCP Sample Server",
"1.0.0"
);
mcpServer.start_processor(); // doesn't block the main thread
}
Tool¶
Implement the feature callbacks and respond with a valid response object:
// inputs are normally validated by the library but
// in the JSON model, a "number" may be both 64-bit integer or float
// that is why we must do double check like this
// this double check is not necessary if the arguments are strings as you may have seen in the "echo" tool
mbase::McpResponseTool add_int64(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
if(std::holds_alternative<int64_t>(in_msg_map.at("num1")) && std::holds_alternative<int64_t>(in_msg_map.at("num2")))
{
int64_t number1 = std::get<int64_t>(in_msg_map.at("num1"));
int64_t number2 = std::get<int64_t>(in_msg_map.at("num2"));
toolResponse.mText = mbase::string::from_format("%llu + %llu = %llu", number1, number2, number1 + number2);
return toolResponse;
}
toolResponse.mText = "Numbers must be 64 bit integers";
return toolResponse;
}
mbase::McpResponseTool add_float64(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
if(std::holds_alternative<double>(in_msg_map.at("num1")) && std::holds_alternative<double>(in_msg_map.at("num2")))
{
double number1 = std::get<double>(in_msg_map.at("num1"));
double number2 = std::get<double>(in_msg_map.at("num2"));
toolResponse.mText = mbase::string::from_format("%f + %f = %f", number1, number2, number1 + number2);
return toolResponse;
}
toolResponse.mText = "Numbers must be 64 floats";
return toolResponse;
}
mbase::McpResponseTool echo(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
toolResponse.mText = std::get<mbase::string>(in_msg_map.at("user_message"));
return toolResponse;
}
Initialize the argument description objects if the tool requires arguments:
mbase::McpToolArgument toolArg1;
toolArg1.mArgType = mbase::McpValueType::NUMBER;
toolArg1.mArgumentName = "num1";
toolArg1.mDescription = "First number of the add_int64/float64 tool";
toolArg1.mIsRequired = true;
mbase::McpToolArgument toolArg2;
toolArg2.mArgType = mbase::McpValueType::NUMBER;
toolArg2.mArgumentName = "num2";
toolArg2.mDescription = "Second number of the add_int64/float64 tool";
toolArg2.mIsRequired = true;
mbase::McpToolArgument echoToolArg;
echoToolArg.mArgType = mbase::McpValueType::STRING;
echoToolArg.mArgumentName = "user_message";
echoToolArg.mDescription = "Message to echo";
echoToolArg.mIsRequired = true;
Initialize the feature description objects:
mbase::McpToolDescription addToolint64Description;
addToolint64Description.mDescription = "This tool adds two 64-bit integers and return the result";
addToolint64Description.mName = "add_int64";
addToolint64Description.mArguments = {toolArg1, toolArg2};
mbase::McpToolDescription addToolfloat64Description;
addToolfloat64Description.mDescription = "This tool adds two 64-bit floating point numbers and return the result";
addToolfloat64Description.mName = "add_float64";
addToolfloat64Description.mArguments = {toolArg1, toolArg2};
mbase::McpToolDescription echoToolDescription;
echoToolDescription.mDescription = "This tool echoes the 'message' argument back to the user";
echoToolDescription.mName = "echo";
echoToolDescription.mArguments = {echoToolArg};
Register the feature descriptions to the server:
mcpServer.register_tool(addToolint64Description, add_int64);
mcpServer.register_tool(addToolfloat64Description, add_float64);
mcpServer.register_tool(echoToolDescription, echo);
Next, we will implement a single resource feature with logging and progress tracking.
Resource¶
Implement the feature callback and respond with a valid response object:
mbase::McpResponseResource content_file_uri(mbase::McpServerClient* in_client_instance, const mbase::Json& in_progress_token)
{
mbase::McpNotificationLogMessage logMsg;
logMsg.mLogger = "resource logger";
logMsg.mError = "Reading content.txt ...";
in_client_instance->send_log(logMsg);
mbase::sleep(500);
in_client_instance->set_progress(25, in_progress_token, "25%");
mbase::sleep(500);
in_client_instance->set_progress(50, in_progress_token, "50%");
mbase::sleep(500);
in_client_instance->set_progress(75, in_progress_token, "75%");
mbase::sleep(500);
in_client_instance->set_progress(100, in_progress_token, "100%");
mbase::McpResponseTextResource textResourceResponse;
textResourceResponse.mMimeType = "text/plain";
textResourceResponse.mText = "This is the content inside the content.txt";
return textResourceResponse;
}
Initialize the resource description object:
mbase::McpResourceDescription contentResourceDescription;
contentResourceDescription.mName = "content.txt";
contentResourceDescription.mUri = "file:///content.txt";
Register the feature description to the server:
mcpServer.register_resource(contentResourceDescription, content_file_uri);
Next, we will implement a prompt with compilation support.
Prompt with Compilation Support¶
Implement the feature callbacks and respond with a valid response object:
mbase::vector<mbase::McpResponsePrompt> greeting_prompt_cb(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextPrompt textPromptResp;
textPromptResp.mRole = "assistant";
textPromptResp.mText = "You must greet the user with the following message: " + std::get<mbase::string>(in_msg_map.at("greet_text"));
return {textPromptResp};
}
mbase::vector<mbase::McpResponsePrompt> mbase_sdk_inform_cb(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextPrompt textPromptResp;
textPromptResp.mRole = "user";
textPromptResp.mText = "Tell me about MBASE " + std::get<mbase::string>(in_msg_map.at("mbase_arg"));
return {textPromptResp};
}
Initialize the argument description objects if the prompt requires arguments:
mbase::McpPromptArgument greetArgument;
greetArgument.mArgumentName = "greet_text";
greetArgument.mDescription = "A greetings text";
greetArgument.mIsRequired = true;
mbase::McpPromptArgument mbaseArgument;
mbaseArgument.mArgumentName = "mbase_arg";
mbaseArgument.mCompletionStrings = {"mcp-sdk", "mcp-sdk-examples", "mcp-sdk-usage", "mcp-server-about", "mcp-server-usage"};
mbaseArgument.mDescription = "What to describe in MBASE MCP SDK";
mbaseArgument.mIsRequired = true;
Initialize the prompt description objects:
mbase::McpPromptDescription greetPromptDescription;
greetPromptDescription.mName = "greeting_prompt";
greetPromptDescription.mDescription = "Will return a greetings prompt template with the argument substituted inside.";
greetPromptDescription.mArguments = {greetArgument};
mbase::McpPromptDescription mbasePromptDescription;
mbasePromptDescription.mName = "mbase_sdk_inform";
mbasePromptDescription.mDescription = "Will return a prompt in the format: 'Tell me about MBASE {argument}'";
mbasePromptDescription.mArguments = {mbaseArgument};
Register the feature descriptions to the server:
mcpServer.register_prompt(greetPromptDescription, greeting_prompt_cb);
mcpServer.register_prompt(mbasePromptDescription, mbase_sdk_inform_cb);
Implementing the Program Loop¶
The server will listen for messages from the client in a parallel transport thread after the call:
mcpServer.start_processor(); // doesn't block the main thread
The server will queue the valid MCP messages in its state or discard the received message if the message is invalid.
Queued messages will be dispatched and all the corresponding callbacks and on_*
events
will be called by the time the update
method of the server is called:
mcpServer.update();
The frequency of this call will determine the dispatch frequency.
For that reason, we need to write a program loop and continuosly call the update method of the server:
while(mcpServer.is_processor_running())
{
mcpServer.update();
mbase::sleep(5); // in order to prevent resource exhaustion
}
The method is_processor_running()
will return true if the server is listening for messages in the transport thread.
Done!¶
Congratulations! You have implemented your first MCP server in C++ with all fundamental features. In order to work with the advanced features of the MCP SDK, refer to the information reference section.
We will now call the features we wrote in this chapter from the MCP C++ client we write in the next chapter.
Complete Source Code¶
#include <mbase/mcp/mcp_server_stdio.h>
#include <mbase/mcp/mcp_server_features.h>
#include <mbase/mcp/mcp_server_responses.h>
// inputs are normally validated by the library but
// the JSON model dictates a "number" which may be both 64-bit integer or float
// that is why we must do double check like this
// this double check is not necessary if the arguments are strings as you may have seen in the "echo" tool
mbase::McpResponseTool add_int64(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
if(std::holds_alternative<int64_t>(in_msg_map.at("num1")) && std::holds_alternative<int64_t>(in_msg_map.at("num2")))
{
int64_t number1 = std::get<int64_t>(in_msg_map.at("num1"));
int64_t number2 = std::get<int64_t>(in_msg_map.at("num2"));
toolResponse.mText = mbase::string::from_format("%llu + %llu = %llu", number1, number2, number1 + number2);
return toolResponse;
}
toolResponse.mText = "Numbers must be 64 bit integers";
return toolResponse;
}
mbase::McpResponseTool add_float64(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
if(std::holds_alternative<double>(in_msg_map.at("num1")) && std::holds_alternative<double>(in_msg_map.at("num2")))
{
double number1 = std::get<double>(in_msg_map.at("num1"));
double number2 = std::get<double>(in_msg_map.at("num2"));
toolResponse.mText = mbase::string::from_format("%f + %f = %f", number1, number2, number1 + number2);
return toolResponse;
}
toolResponse.mText = "Numbers must be 64 floats";
return toolResponse;
}
mbase::McpResponseTool echo(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextTool toolResponse;
toolResponse.mText = std::get<mbase::string>(in_msg_map.at("user_message"));
return toolResponse;
}
mbase::McpResponseResource content_file_uri(mbase::McpServerClient* in_client_instance, const mbase::Json& in_progress_token)
{
mbase::McpNotificationLogMessage logMsg;
logMsg.mLogger = "resource logger";
logMsg.mError = "Reading content.txt ...";
in_client_instance->send_log(logMsg);
mbase::sleep(500);
in_client_instance->set_progress(25, in_progress_token, "25%");
mbase::sleep(500);
in_client_instance->set_progress(50, in_progress_token, "50%");
mbase::sleep(500);
in_client_instance->set_progress(75, in_progress_token, "75%");
mbase::sleep(500);
in_client_instance->set_progress(100, in_progress_token, "100%");
mbase::McpResponseTextResource textResourceResponse;
textResourceResponse.mMimeType = "text/plain";
textResourceResponse.mText = "This is the content inside the content.txt";
return textResourceResponse;
}
mbase::vector<mbase::McpResponsePrompt> greeting_prompt_cb(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextPrompt textPromptResp;
textPromptResp.mRole = "assistant";
textPromptResp.mText = "You must greet the user with the following message: " + std::get<mbase::string>(in_msg_map.at("greet_text"));
return {textPromptResp};
}
mbase::vector<mbase::McpResponsePrompt> mbase_sdk_inform_cb(mbase::McpServerClient* in_client_instance, const mbase::McpMessageMap& in_msg_map, const mbase::Json& in_progress_token)
{
mbase::McpResponseTextPrompt textPromptResp;
textPromptResp.mRole = "user";
textPromptResp.mText = "Tell me about MBASE " + std::get<mbase::string>(in_msg_map.at("mbase_arg"));
return {textPromptResp};
}
int main()
{
mbase::McpServerStdio mcpServer(
"MCP Sample Server",
"1.0.0"
);
mcpServer.start_processor();
mbase::McpToolArgument toolArg1;
toolArg1.mArgType = mbase::McpValueType::NUMBER;
toolArg1.mArgumentName = "num1";
toolArg1.mDescription = "First number of the add_int64/float64 tool";
toolArg1.mIsRequired = true;
mbase::McpToolArgument toolArg2;
toolArg2.mArgType = mbase::McpValueType::NUMBER;
toolArg2.mArgumentName = "num2";
toolArg2.mDescription = "Second number of the add_int64/float64 tool";
toolArg2.mIsRequired = true;
mbase::McpToolArgument echoToolArg;
echoToolArg.mArgType = mbase::McpValueType::STRING;
echoToolArg.mArgumentName = "user_message";
echoToolArg.mDescription = "Message to echo";
echoToolArg.mIsRequired = true;
mbase::McpPromptArgument greetArgument;
greetArgument.mArgumentName = "greet_text";
greetArgument.mDescription = "A greetings text";
greetArgument.mIsRequired = true;
mbase::McpPromptArgument mbaseArgument;
mbaseArgument.mArgumentName = "mbase_arg";
mbaseArgument.mCompletionStrings = {"mcp-sdk", "mcp-sdk-examples", "mcp-sdk-usage", "mcp-server-about", "mcp-server-usage"};
mbaseArgument.mDescription = "What to describe in MBASE MCP SDK";
mbaseArgument.mIsRequired = true;
mbase::McpToolDescription addToolint64Description;
addToolint64Description.mDescription = "This tool adds two 64-bit integers and return the result";
addToolint64Description.mName = "add_int64";
addToolint64Description.mArguments = {toolArg1, toolArg2};
mbase::McpToolDescription addToolfloat64Description;
addToolfloat64Description.mDescription = "This tool adds two 64-bit floating point numbers and return the result";
addToolfloat64Description.mName = "add_float64";
addToolfloat64Description.mArguments = {toolArg1, toolArg2};
mbase::McpToolDescription echoToolDescription;
echoToolDescription.mDescription = "This tool echoes the 'message' argument back to the user";
echoToolDescription.mName = "echo";
echoToolDescription.mArguments = {echoToolArg};
mbase::McpResourceDescription contentResourceDescription;
contentResourceDescription.mName = "content_file_uri";
contentResourceDescription.mUri = "file:///content.txt";
mbase::McpPromptDescription greetPromptDescription;
greetPromptDescription.mName = "greeting_prompt";
greetPromptDescription.mDescription = "Will return a greetings prompt template with the argument substituted inside.";
greetPromptDescription.mArguments = {greetArgument};
mbase::McpPromptDescription mbasePromptDescription;
mbasePromptDescription.mName = "mbase_sdk_inform";
mbasePromptDescription.mDescription = "Will return a prompt in the format: 'Tell me about MBASE {argument}'";
mbasePromptDescription.mArguments = {mbaseArgument};
mcpServer.register_tool(addToolint64Description, add_int64);
mcpServer.register_tool(addToolfloat64Description, add_float64);
mcpServer.register_tool(echoToolDescription, echo);
mcpServer.register_resource(contentResourceDescription, content_file_uri);
mcpServer.register_prompt(greetPromptDescription, greeting_prompt_cb);
mcpServer.register_prompt(mbasePromptDescription, mbase_sdk_inform_cb);
while(mcpServer.is_processor_running())
{
mcpServer.update();
mbase::sleep(5);
}
return 0;
}