MCP Protocol IoT Control Usage Guide

This document describes how to implement ESP32 device IoT control based on MCP protocol. For detailed protocol flow, please refer to MCP Protocol Documentation.

Introduction

MCP (Model Context Protocol) is a next-generation protocol recommended for IoT control. It discovers and calls “tools” between backend and devices through standard JSON-RPC 2.0 format, enabling flexible device control.

Typical Usage Flow

  1. Device establishes connection with backend via underlying protocol (like WebSocket/MQTT) after startup.
  2. Backend initializes session through MCP protocol’s initialize method.
  3. Backend gets all tools (functions) supported by device and parameter descriptions via tools/list.
  4. Backend calls specific tools via tools/call to achieve device control.

For detailed protocol format and interaction, see MCP Protocol Documentation.

Device-side Tool Registration Method

Devices register callable “tools” for backend via McpServer::AddTool method. Common function signature:

void AddTool(
    const std::string& name,           // Tool name, suggest unique and hierarchical, like self.dog.forward
    const std::string& description,    // Tool description, brief functionality explanation for LLM understanding
    const PropertyList& properties,    // Input parameter list (can be empty), supports: bool, int, string
    std::function<ReturnValue(const PropertyList&)> callback // Callback implementation when tool is called
);

Parameter Description

  • name: Tool unique identifier, suggest using “module.function” naming style.
  • description: Natural language description for AI/user understanding.
  • properties: Parameter list, supports bool, int, string types, can specify range and default values.
  • callback: Actual execution logic when receiving call request, return value can be bool/int/string.

Typical Registration Examples (Using ESP-Hi)

void InitializeTools() {
    auto& mcp_server = McpServer::GetInstance();
    
    // Example 1: No parameters, control robot to move forward
    mcp_server.AddTool("self.dog.forward", "Robot moves forward", PropertyList(), 
        [this](const PropertyList&) -> ReturnValue {
            servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
            return true;
        });
    
    // Example 2: With parameters, set light RGB color
    mcp_server.AddTool("self.light.set_rgb", "Set RGB color", PropertyList({
        Property("r", kPropertyTypeInteger, 0, 255),
        Property("g", kPropertyTypeInteger, 0, 255),
        Property("b", kPropertyTypeInteger, 0, 255)
    }), [this](const PropertyList& properties) -> ReturnValue {
        int r = properties["r"].value<int>();
        int g = properties["g"].value<int>();
        int b = properties["b"].value<int>();
        led_on_ = true;
        SetLedColor(r, g, b);
        return true;
    });
}

Common Tool Call JSON-RPC Examples

1. Get Tool List

{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "params": { "cursor": "" },
  "id": 1
}

2. Control Chassis Forward

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "self.chassis.go_forward",
    "arguments": {}
  },
  "id": 2
}

3. Switch Light Mode

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "self.chassis.switch_light_mode",
    "arguments": { "light_mode": 3 }
  },
  "id": 3
}

4. Camera Flip

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "self.camera.set_camera_flipped",
    "arguments": {}
  },
  "id": 4
}

Tool Registration Best Practices

1. Naming Conventions

  • Use hierarchical naming: self.module.function
  • Keep names concise and descriptive
  • Avoid special characters and spaces
// Good naming examples
"self.light.set_brightness"
"self.motor.rotate_left"  
"self.sensor.get_temperature"
"self.display.show_text"
// Discouraged naming
"light-control"  // Lacks hierarchy
"setBrightness"  // Lacks module info
"控制灯光"       // Non-English naming
"self.light.set brightness" // Contains spaces

2. Parameter Design

  • Use clear parameter names
  • Set reasonable parameter ranges and default values
  • Provide detailed parameter descriptions
// Parameter design example
PropertyList({
    Property("volume", kPropertyTypeInteger, 0, 100, "Volume level, range 0-100"),
    Property("enabled", kPropertyTypeBool, true, "Whether to enable function"),
    Property("message", kPropertyTypeString, "", "Text content to display")
})

3. Error Handling

Include appropriate error handling in tool callbacks:

mcp_server.AddTool("self.servo.set_angle", "Set servo angle", PropertyList({
    Property("angle", kPropertyTypeInteger, 0, 180)
}), [this](const PropertyList& properties) -> ReturnValue {
    try {
        int angle = properties["angle"].value<int>();
        if (angle < 0 || angle > 180) {
            return "Angle out of range: 0-180";
        }
        SetServoAngle(angle);
        return true;
    } catch (const std::exception& e) {
        return std::string("Failed to set angle: ") + e.what();
    }
});

Debugging Tips

1. Logging

Add logging in tool implementations:

mcp_server.AddTool("self.debug.test", "Test tool", PropertyList(), 
    [this](const PropertyList& properties) -> ReturnValue {
        ESP_LOGI("MCP", "Test tool called");
        // Execute test logic
        return "Test completed";
    });

2. Parameter Validation

Add detailed parameter validation:

int volume = properties["volume"].value<int>();
if (volume < 0 || volume > 100) {
    ESP_LOGE("MCP", "Invalid volume parameter: %d", volume);
    return "Volume must be between 0-100";
}

Notes

  • Tool names, parameters and return values should be based on device-side AddTool registration.
  • All new projects are recommended to uniformly adopt MCP protocol for IoT control.
  • For detailed protocol and advanced usage, please refer to MCP Protocol Documentation.

Related Documentation

Frequently Asked Questions

Q: How to handle tool call timeouts?

A: Avoid long blocking operations in tool callbacks. For time-consuming operations, handle asynchronously and return status immediately.

Q: Can tools be dynamically added or removed?

A: Current implementation typically registers tools at device startup. Dynamic management requires redesigning the tool registration mechanism.

Q: How to handle tool call permissions?

A: Permission check logic can be added in tool callbacks, or permission control can be implemented at the backend API layer.