Exercise : Hello MCP

基本信息

项目 内容
难度
预估时间 1-2 小时
核心技能 JSON-RPC 2.0, stdin/stdout 传输, nlohmann/json
产出物 一个可运行的最简 MCP 服务器 (约 100 行代码)

学习目标

  • 理解 MCP 协议中最基础的请求-响应循环
  • 掌握 stdin/stdout 行协议
  • 使用 nlohmann/json 解析和构造 JSON 消息
  • 理解 JSON-RPC 的 id 字段和 method 字段

前置准备

阅读以下文档的相关章节:

需求规格

实现一个最简单可能的 MCP 服务器,支持以下功能:

  1. 通过 stdin 读取 JSON-RPC 请求(每行一个请求)
  2. 支持 initialize 命令:返回服务器能力和版本
  3. 支持 ping 命令:返回空对象 {}
  4. 支持 tools/list 命令:返回一个硬编码的工具列表(包含一个 “echo” 工具)
  5. 支持 tools/call 命令:如果调用 “echo”,返回用户传入的文本
  6. 通过 stdout 返回 JSON-RPC 响应(每行一个响应)
  7. 对未知命令返回 Method not found 错误

步骤指引

Step 1: 项目骨架 (15分钟)

// hello_mcp.cpp
#include <iostream>
#include <string>
#include "json.hpp"

using json = nlohmann::json;

int main() {
    std::string line;
    while (std::getline(std::cin, line)) {
        // 处理每一行请求
        if (line.empty()) continue;
        
        try {
            auto request = json::parse(line);
            auto response = handle_request(request);
            std::cout << response.dump() << std::endl;
            std::cout.flush();  // 重要: 立即刷新
        } catch (const json::parse_error& e) {
            // 返回 Parse error
            json error_response;
            error_response["jsonrpc"] = "2.0";
            error_response["id"] = nullptr;
            error_response["error"] = {
                {"code", -32700},
                {"message", "Parse error"}
            };
            std::cout << error_response.dump() << std::endl;
        }
    }
    return 0;
}

Step 2: 实现命令处理函数 (20分钟)

json handle_request(const json& request) {
    std::string method = request.value("method", "");
    
    if (method == "initialize") {
        return handle_initialize(request);
    } else if (method == "ping") {
        return handle_ping(request);
    } else if (method == "tools/list") {
        return handle_tools_list(request);
    } else if (method == "tools/call") {
        return handle_tools_call(request);
    } else {
        // Method not found
        json error;
        error["jsonrpc"] = "2.0";
        error["id"] = request.value("id", nullptr);
        error["error"] = {
            {"code", -32601},
            {"message", "Method not found: " + method}
        };
        return error;
    }
}

Step 3: 实现各个命令 (20分钟)

json handle_initialize(const json& request) {
    json response;
    response["jsonrpc"] = "2.0";
    response["id"] = request["id"];
    response["result"] = {
        {"protocolVersion", "2024-11-05"},
        {"serverInfo", {
            {"name", "hello-mcp"},
            {"version", "1.0.0"}
        }},
        {"capabilities", {
            {"tools", {{"listChanged", false}}}
        }}
    };
    return response;
}

json handle_ping(const json& request) {
    json response;
    response["jsonrpc"] = "2.0";
    response["id"] = request["id"];
    response["result"] = json::object();
    return response;
}

json handle_tools_list(const json& request) {
    json response;
    response["jsonrpc"] = "2.0";
    response["id"] = request["id"];
    
    json echo_tool;
    echo_tool["name"] = "echo";
    echo_tool["description"] = "Echoes back the input text";
    echo_tool["inputSchema"] = {
        {"type", "object"},
        {"properties", {
            {"text", {{"type", "string"}, {"description", "Text to echo"}}}
        }},
        {"required", json::array({"text"})}
    };
    
    response["result"]["tools"] = json::array({echo_tool});
    return response;
}

json handle_tools_call(const json& request) {
    std::string tool_name = request["params"]["name"];
    
    if (tool_name == "echo") {
        std::string text = request["params"]["arguments"]["text"];
        
        json response;
        response["jsonrpc"] = "2.0";
        response["id"] = request["id"];
        response["result"] = {
            {"content", json::array({
                {{"type", "text"}, {"text", "Echo: " + text}}
            })},
            {"isError", false}
        };
        return response;
    }
    
    // Tool not found
    json error;
    error["jsonrpc"] = "2.0";
    error["id"] = request["id"];
    error["error"] = {
        {"code", -32602},
        {"message", "Unknown tool: " + tool_name}
    };
    return error;
}

Step 4: 编译和测试 (15分钟)

# 编译
g++ -std=c++17 hello_mcp.cpp -I../libs_tier_01/json/include -o hello_mcp

# 测试 initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | ./hello_mcp

# 测试 tools/list
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./hello_mcp

# 测试 tools/call
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"Hello MCP!"}}}' | ./hello_mcp

# 测试未知命令
echo '{"jsonrpc":"2.0","id":4,"method":"unknown"}' | ./hello_mcp

期望输出

initialize 请求的输出应类似:

{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":false}},"protocolVersion":"2024-11-05","serverInfo":{"name":"hello-mcp","version":"1.0.0"}}}

提示

  1. std::cout.flush() 很关键 – stdin/stdout 是行缓冲的,不 flush 客户端收不到响应
  2. request.value("id", nullptr) 安全地获取 id 字段(JSON-RPC 通知没有 id)
  3. 空行要跳过,否则 json::parse("") 会抛异常

扩展挑战

  • 添加 prompts/list 命令,返回一个硬编码的提示词
  • 添加请求日志(输出到 stderr,避免干扰 stdout 协议)
  • 支持多个工具(添加 “time” 工具返回当前时间)
  • 让你的 Hello MCP 能被 Claude Desktop 直接加载使用

My Answer

#include <iostream>
#include <nlohmann/json.hpp>
#include <unordered_map>

using json = nlohmann::json;

/// @brief 构造一个空的响应消息
/// @param request :请求消息
/// @return 空的响应消息
json Response(const json &request)
{
    json response;
    response["jsonrpc"] = "2.0";
    response["id"] = request["id"];
    response["result"] = json::object();
    return response;
}

enum class ErrorCode
{
    // JSON解析失败
    ParseError = -32700,
    // 请求对象无效
    InvalidRequest = -32600,
    // 请求的方法不存在
    MethodNotFound = -32601,
    // 方法参数无效
    InvalidParams = -32602,
    // 服务器内部错误
    InternalError = -32603
};

class Server
{
public:
    Server()
    {
        FunctionMap = {
            {"initialize", [this](const json &request)
             { return this->InitializeImpl(request); }},
            {"ping", [this](const json &request)
             { return this->PingImpl(request); }},
            {"tools/list", [this](const json &request)
             { return this->ToolsListImpl(request); }},
            {"tools/call", [this](const json &request)
             { return this->ToolsCallImpl(request); }}};
    }

    void start()
    {
        json response, request;
        std::string message;
        while (true)
        {
            if (!std::getline(std::cin, message) || message.empty())
            {
                break; // EOF or empty line, exit gracefully
            }
            try
            {
                request = json::parse(message);
            }
            catch (const json::parse_error &e)
            {
                response = Error(ErrorCode::ParseError, "null", e.what());
                std::cout << response.dump() << std::endl;
                continue;
            }
            std::string method;
            if (request.contains("method"))
            {
                method = request["method"].get<std::string>();
                auto it = FunctionMap.find(method);
                if (it != FunctionMap.end())
                {
                    response = FunctionMap[method](request);
                    std::cout << response.dump() << std::endl;
                }
                else
                {
                    response = Error(ErrorCode::MethodNotFound, method, "Method not Found");
                    std::cout << response.dump() << std::endl;
                }
            }
            else
            {
                response = Error(ErrorCode::InvalidRequest, request["id"].dump(), "Missing method");
                std::cout << response.dump() << std::endl;
            }
        }
    }

private:
    json Error(ErrorCode code, const std::string &id, const std::string &message)
    {
        return {
            {"jsonrpc", "2.0"},
            {"error", {{"code", static_cast<int>(code)}, {"message", message}}},
            {"id", id}};
    }

    json InitializeImpl(const json &request)
    {
        json response = Response(request);
        if (!request.contains("params"))
        {
            return Error(ErrorCode::InvalidParams, request["id"].dump(), "Missing Params");
        }
        if(request["params"].empty()){
            response["result"]["protocolVersion"] = json::object();
        }else{
            response["result"]["protocolVersion"] = request["params"]["protocolVersion"];
        }

        response["result"]["capabilities"]["tools"] = json::object({{"listChanged", true}});
        response["result"]["capabilities"]["prompts"] = json::object({{"listChanged", true}});
        response["result"]["capabilities"]["resources"]["subscribe"] = true;
        response["result"]["capabilities"]["resources"]["listChanged"] = true;
        response["result"]["capabilities"]["logging"] = json::object();
        return response;
    }

    json PingImpl(const json &request)
    {
        return Response(request);
    }

    json ToolsListImpl(const json &request)
    {
        json response = Response(request);
        json tool;
        tool["name"] = "echo";
        tool["description"] = "server echo message from user";
        tool["inputSchema"] = json::object();
        response["result"]["tools"] = json::array();
        response["result"]["tools"].push_back(tool);
        return response;
    }

    json ToolsCallImpl(const json &request)
    {
        json response = Response(request);
        if (!request.contains("params"))
        {
            return Error(ErrorCode::InvalidParams, request["id"].dump(), "Missing Params");
        }
        if (request["params"]["name"] == "echo")
        {
            json context;
            context["type"] = "text";
            context["text"] = request["params"]["arguments"];
            response["result"]["content"] = json::array();
            response["result"]["content"].push_back(context);
        }
        else
        {
            response["result"]["content"] = json::object();
        }
        return response;
    }

private:
    std::unordered_map<std::string, std::function<json(const json &)>> FunctionMap;
};

int main()
{
    Server server;
    server.start();
    return 0;
}

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐