mcp/mcp_server

Example of the component espressif/mcp-c-sdk v2.0.0
# MCP Server 示例

本示例展示如何在 ESP32 上实现 **Model Context Protocol (MCP)** 服务端,为 AI 应用提供标准化工具接口。示例覆盖设备状态查询、音量控制、亮度调节、主题切换、颜色控制,以及四模块能力(Resource / Prompt / Completion / Tasks)。

## 功能特性

* 设备状态查询(JSON 返回)
* 音量控制(0-100)及范围校验
* 屏幕亮度调节(0-100)
* 主题切换(light/dark)
* HSV 颜色控制(数组参数)
* RGB 颜色控制(对象参数)
* 资源注册与读取(`resources/list`, `resources/read`)
* Prompt 注册与渲染(`prompts/list`, `prompts/get`)
* 参数补全 Provider(`completion/complete`)
* 任务模式工具调用(`tools/call` + `params.task={}`,并可用 `tasks/*` 轮询)
* 完整 JSON-RPC 2.0 支持
* 内置参数校验
* 工具与属性链表的线程安全管理(mutex 保护)
* HTTP 传输支持

## 硬件要求

* 搭载 ESP32/ESP32-S2/ESP32-S3/ESP32-C3/ESP32-C6 SoC 的开发板
* USB 数据线(供电与烧录)
* Wi-Fi 路由器

## 使用方法

在配置和构建前,请先设置正确芯片目标:`idf.py set-target <chip_name>`。

### 配置工程

打开 menuconfig:

```bash
idf.py menuconfig
```

在 `Example Connection Configuration` 菜单下:
* 设置 Wi-Fi SSID
* 设置 Wi-Fi Password

### 编译与烧录

```bash
idf.py -p PORT flash monitor
```

(退出串口监视器请按 `Ctrl-]`)

完整构建流程可参考 [ESP-IDF Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html)。

## 运行输出示例

烧录后,设备联网并启动 MCP 服务端:

```text
I (2543) wifi:connected with MySSID, aid = 1, channel 6, BW20, bssid = xx:xx:xx:xx:xx:xx
I (3542) example_connect: - IPv4 address: 192.168.1.100
I (3552) mcp_server: MCP Server started on port 80
I (3553) mcp_server: MCP endpoint registered: /mcp_server
```

## 服务端测试

服务端启动后,可用 `curl` 或任意 HTTP 客户端测试。

### 1. 获取工具列表

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'
```

### 2. 获取设备状态

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "self.get_device_status",
      "arguments": {}
    }
  }'
```

### 3. 设置音量

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "self.audio_speaker.set_volume",
      "arguments": {
        "volume": 75
      }
    }
  }'
```

### 4. 设置亮度

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"self.screen.set_brightness","arguments":{"brightness":60}}}'
```

### 5. 设置主题

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"self.screen.set_theme","arguments":{"theme":"dark"}}}'
```

### 6. 设置 HSV

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"self.screen.set_hsv","arguments":{"HSV":[180,50,80]}}}'
```

### 7. 设置 RGB

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"self.screen.set_rgb","arguments":{"RGB":{"red":255,"green":128,"blue":0}}}}'
```

### 8. 列出资源

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":8,"method":"resources/list","params":{}}'
```

### 9. 读取资源

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"device://status"}}'
```

### 10. 列出资源模板

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":10,"method":"resources/templates/list","params":{}}'
```

### 11. 订阅资源更新

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":11,"method":"resources/subscribe","params":{"uri":"device://status"}}'
```

### 12. 取消订阅资源更新

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":12,"method":"resources/unsubscribe","params":{"uri":"device://status"}}'
```

### 13. 列出 Prompt

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":13,"method":"prompts/list","params":{}}'
```

### 14. 获取 Prompt

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":14,"method":"prompts/get","params":{"name":"status.summary","arguments":{"device":"screen"}}}'
```

### 15. 参数补全

```bash
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":15,"method":"completion/complete","params":{"ref":{"type":"ref/prompt","name":"status.summary"},"argument":{"name":"device","value":"s"}}}'
```

### 16. 任务模式工具调用与轮询

```bash
# 启动异步任务(返回值在 result.task 中包含任务信息)
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":16,"method":"tools/call","params":{"name":"self.get_device_status","arguments":{},"task":{}}}'

# 查询任务列表
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":17,"method":"tasks/list","params":{}}'

# 按 task id 查询状态和结果
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":18,"method":"tasks/get","params":{"taskId":"<task_id>"}}'

curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":19,"method":"tasks/result","params":{"taskId":"<task_id>"}}'

# 说明:tasks/result 返回的是底层 tools/call 的结果对象(如 content/isError),
# 响应里不会重复回显 taskId。

# 取消任务
curl -X POST http://192.168.1.100/mcp_server \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":20,"method":"tasks/cancel","params":{"taskId":"<task_id>"}}'
```

`notifications/resources/updated` 属于服务端通知,需通过 Streamable HTTP SSE 下发。  
`notifications/resources/list_changed` 在资源注册表变化时触发(例如资源 add/remove)。  
本示例里的 `self.resource.touch_status` 会刻意执行 remove+add,因此会观察到这两类通知。  
资源订阅是会话级(session-scoped)的,POST 与 SSE 需要使用同一个 `MCP-Session-Id` 才能观察该会话的更新通知。  
如果当前未启用 SSE(GET 返回 405),仍可先验证 `resources/subscribe` / `resources/unsubscribe` 的请求响应行为。

### 17. 打开 SSE 流(GET)

```bash
curl -N http://192.168.1.100/mcp_server \
  -H "Accept: text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25"
```

### 17.1 纯 SSE 入口(/mcp_server_sse)

启用 SSE 时,服务端会额外注册一个仅 SSE 的入口:`/mcp_server_sse`。  
该入口要求所有 POST 请求必须带 `MCP-Session-Id`(来自 SSE 响应头),
HTTP 层返回 202/空 body,JSON-RPC 响应通过 SSE 事件下发。

```bash
# 1) 打开 SSE 流并记录响应头里的 MCP-Session-Id
curl -N -D /tmp/mcp_sse.hdr http://192.168.1.100/mcp_server_sse \
  -H "Accept: text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25"

# 2) 使用 session id 发送 POST(响应会通过 SSE 返回)
SESSION_ID="$(sed -nE 's/^MCP-Session-Id:[[:space:]]*([^[:space:]\r]+).*/\1/ip' /tmp/mcp_sse.hdr | sed -n '1p' | tr -d '\r')"
curl -i -X POST http://192.168.1.100/mcp_server_sse \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -H "MCP-Session-Id: ${SESSION_ID}" \
  -d '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}'
```

### 18. SSE 端到端可观测测试脚本

```bash
# 默认执行
bash test_sse_e2e.sh 192.168.1.100 mcp_server

# 打印每个 JSON-RPC 响应
bash test_sse_e2e.sh 192.168.1.100 mcp_server --verbose

# 脚本退出后保留 SSE 日志文件
bash test_sse_e2e.sh 192.168.1.100 mcp_server --keep-log

# 严格模式:对响应关键字段做断言
bash test_sse_e2e.sh 192.168.1.100 mcp_server --strict
```

> **说明:** 该脚本基于 Streamable HTTP(POST 直接返回 JSON),请对 `/mcp_server` 使用;  
> `/mcp_server_sse` 为纯 SSE 入口,POST 返回 202,响应在 SSE 流中返回。

脚本是示例功能的全链路 smoke test,覆盖:
- `initialize` + `notifications/initialized`
- `ping`(包含字符串 `id`)
- `tools/list`
- 示例里全部 `tools/call`:
  - `self.get_device_status`
  - `self.audio_speaker.set_volume`
  - `self.screen.set_brightness`
  - `self.screen.set_theme`
  - `self.screen.set_rgb`
  - `self.screen.set_hsv`
  - `self.resource.touch_status`
- `resources/list`, `resources/read`, `resources/templates/list`, `resources/subscribe`, `resources/unsubscribe`
- `prompts/list`, `prompts/get`
- `completion/complete`
- 任务模式(`tools/call` + `task={}`)以及 `tasks/get`, `tasks/result`, `tasks/cancel`
- JSON-RPC batch 处理(request + notification + error 混合)
- JSON-RPC 错误包与 `error.data` 校验(如 `method`、`uri`、`expected`)
- SSE 校验 `notifications/resources/updated` 与 `notifications/resources/list_changed`
- SSE heartbeat 注释(`: ping`)与携带 `MCP-Session-Id` + `Last-Event-ID` 的重连流程

`--strict` 会额外校验响应中的关键字段,例如:
- `resources/templates/list` 必须包含 `device://sensors/{id}`
- `prompts/list` 必须包含 `status.summary`
- 任务模式响应必须包含 `taskId`
- batch/error 响应必须包含预期 id/code 与 `error.data`
- 重连后的 SSE 流仍可观测到通知

> **说明:** 严格模式会等待 SSE heartbeat,因此总时长会更长(默认额外约 15-20 秒)。

**说明:** 示例中 endpoint 为 `/mcp_server`,由 `esp_mcp_mgr_register_endpoint()` 注册。你可以按需修改路径。  
启用 SSE 时,会额外提供 `/mcp_server_sse`(纯 SSE 入口,POST 响应通过 SSE 下发)。

## 已注册工具

| 工具名 | 参数 | 说明 |
|--------|------|------|
| `self.get_device_status` | 无 | 获取设备状态(JSON) |
| `self.audio_speaker.set_volume` | `volume` (0-100) | 设置音量 |
| `self.screen.set_brightness` | `brightness` (0-100) | 设置亮度 |
| `self.screen.set_theme` | `theme` (string) | 设置主题 |
| `self.screen.set_hsv` | `HSV` (array[3]) | 设置 HSV |
| `self.screen.set_rgb` | `RGB` (object) | 设置 RGB |
| `self.resource.touch_status` | 无 | 触发 `notifications/resources/updated` 以测试 SSE |
| `self.server.roots_list` | 无 | 向当前会话发送服务端主动 `roots/list` 请求 |
| `self.server.sampling_create` | 无 | 向当前会话发送服务端主动 `sampling/create` 请求 |
| `self.server.elicitation_request` | 无 | 向当前会话发送服务端主动 `elicitation/request` 请求 |

## 已注册 Resource / Prompt / Completion

| 类型 | 名称/URI | 说明 |
|------|----------|------|
| Resource | `device://status` | 返回当前模拟设备状态 JSON |
| Resource Template | `device://sensors/{id}` | 按传感器维度组织的模板 URI |
| Prompt | `status.summary` | 生成设备状态总结 Prompt |
| Completion | 参数 `theme` / `device` | 返回建议补全值 |

## 属性类型示例

```c
// 整型(带范围)
esp_mcp_property_t *property = esp_mcp_property_create_with_range("volume", 0, 100);
esp_mcp_tool_add_property(tool, property);

// 整型(带默认值)
property = esp_mcp_property_create_with_int("param", 10);
esp_mcp_tool_add_property(tool, property);

// 布尔(带默认值)
property = esp_mcp_property_create_with_bool("enabled", true);
esp_mcp_tool_add_property(tool, property);

// 字符串(带默认值)
property = esp_mcp_property_create_with_string("theme", "light");
esp_mcp_tool_add_property(tool, property);

// 数组(JSON 字符串默认值)
property = esp_mcp_property_create_with_array("HSV", "[0, 100, 360]");
esp_mcp_tool_add_property(tool, property);

// 对象(JSON 字符串默认值)
property = esp_mcp_property_create_with_object("RGB", "{\"red\":0,\"green\":120,\"blue\":240}");
esp_mcp_tool_add_property(tool, property);
```

## 故障排查

* **Wi-Fi 连接失败**:检查 `menuconfig` 中的 Wi-Fi 配置,并确保设备在覆盖范围内
* **无法访问 MCP 服务**:确认设备 IP,确保客户端与设备在同一局域网
* **工具调用报错**:检查 JSON-RPC 格式与参数值

## 传输支持

### HTTP 传输

示例默认使用 HTTP 传输,MCP 服务端通过 HTTP POST 接收 JSON-RPC 2.0 请求。

```c
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
esp_mcp_mgr_config_t mcp_mgr_config = {
    .transport = esp_mcp_transport_http_server,
    .config = &http_config,
    .instance = mcp,
};
ESP_ERROR_CHECK(esp_mcp_mgr_init(mcp_mgr_config, &mcp_mgr_handle));
ESP_ERROR_CHECK(esp_mcp_mgr_start(mcp_mgr_handle));
ESP_ERROR_CHECK(esp_mcp_mgr_register_endpoint(mcp_mgr_handle, "mcp_server", NULL));
```

**默认 endpoint:** `/mcp_server`  
**默认端口:** 80

### 自定义传输

SDK 支持自定义传输实现,详情见 `components/mcp-c-sdk/README.md`。

## 扩展自定义工具

1. 定义工具回调:

```c
static esp_mcp_value_t my_tool_callback(const esp_mcp_property_list_t* properties)
{
    int value = esp_mcp_property_list_get_property_int(properties, "param");
    ESP_LOGI(TAG, "Tool called with param: %d", value);
    return esp_mcp_value_create_bool(true);
}
```

2. 创建工具:

```c
esp_mcp_tool_t *tool = esp_mcp_tool_create("my_tool", "Tool description", my_tool_callback);
```

3. 添加属性(可选):

```c
esp_mcp_property_t *property = esp_mcp_property_create_with_int("param", 10);
esp_mcp_tool_add_property(tool, property);
```

4. 注册工具:

```c
esp_mcp_add_tool(mcp, tool);
```

## 技术参考

* [Model Context Protocol Specification](https://modelcontextprotocol.io/)
* [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/)
* [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)

To create a project from this example, run:

idf.py create-project-from-example "espressif/mcp-c-sdk=2.0.0:mcp/mcp_server"

or download archive (~39.43 KB)