Skip to content

Commit f7789cb

Browse files
author
Nicola Spieser
committed
docs: async_tasks example, ARCHITECTURE.md, expanded API docs — bump to v0.33.1
1 parent deabec4 commit f7789cb

9 files changed

Lines changed: 500 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.33.1] — 2026-02-22
6+
7+
### Added
8+
- **`async_tasks` example**: Full Arduino sketch demonstrating the Tasks feature — async firmware update with progress tracking, interactive sensor calibration with `InputRequired` flow, and comparison with synchronous tools.
9+
- **Architecture documentation** (`docs/ARCHITECTURE.md`): Comprehensive technical overview of the codebase — component diagram, transport layer, feature modules, design decisions, test architecture, memory budget, and message flow.
10+
- **Tasks API documentation**: Updated `docs/API.md` with full Tasks reference — enabling tasks, registering async tools, task lifecycle, server-side control methods, JSON-RPC methods, and task-augmented `tools/call`.
11+
- **Tool call hooks documentation**: Added `onBeforeToolCall()` and `onAfterToolCall()` API reference to docs.
12+
- **Server instructions & icons documentation**: Added `setInstructions()` and `addIcon()` API reference to docs.
13+
514
## [0.33.0] — 2026-02-22
615

716
### Added

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ mqtt.attach(mcp);
397397
// In loop(): mqtt.loop();
398398
```
399399
400-
For full API documentation, see [docs/API.md](docs/API.md).
400+
For full API documentation, see [docs/API.md](docs/API.md). For a technical overview of the codebase, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
401401
402402
## Examples
403403
@@ -410,6 +410,7 @@ For full API documentation, see [docs/API.md](docs/API.md).
410410
| [`mqtt_bridge`](examples/mqtt_bridge/) | MQTT pub/sub bridge — AI talks to IoT | ESP32 + MQTT broker |
411411
| [`robot_arm`](examples/robot_arm/) | Claude controls a 4-DOF servo robot arm | ESP32 + 4× servos |
412412
| [`smart_greenhouse`](examples/smart_greenhouse/) | Greenhouse automation with logging & dynamic tools | ESP32 + DHT22 + relays |
413+
| [`async_tasks`](examples/async_tasks/) | Async tool execution with progress tracking & input flow | ESP32 only |
413414
414415
## Supported Platforms
415416

docs/API.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ mcpd implements MCP specification 2025-03-26 via Streamable HTTP transport.
289289
| `completion/complete` | Get autocomplete suggestions for prompt args or resource template vars |
290290
| `resources/subscribe` | Subscribe to change notifications for a resource URI |
291291
| `resources/unsubscribe` | Unsubscribe from resource change notifications |
292+
| `tasks/get` | Get status of a task by ID |
293+
| `tasks/list` | List all tasks (with pagination) |
294+
| `tasks/result` | Get the result of a completed task |
295+
| `tasks/cancel` | Cancel a running task |
292296

293297
### Tool Output Schema & Structured Content
294298

@@ -319,6 +323,129 @@ mcp.addResourceTemplate(tmpl);
319323
- `audience`: `"user"` or `"assistant"` — hints who the content is primarily for
320324
- `priority`: `0.0` to `1.0` — relative importance hint for ordering/filtering
321325

326+
### Tasks (Async Tool Execution)
327+
328+
*MCP 2025-11-25 experimental*
329+
330+
Tasks allow tools to execute asynchronously. The client starts a tool call as a task, receives a task ID, and polls for status until completion.
331+
332+
#### Enabling Tasks
333+
334+
```cpp
335+
mcp.enableTasks(); // Call before begin()
336+
```
337+
338+
#### Registering Async Tools
339+
340+
```cpp
341+
mcp.addTaskTool(
342+
"firmware_update",
343+
"Start an OTA firmware update",
344+
R"({"type":"object","properties":{"url":{"type":"string"}},"required":["url"]})",
345+
[](const String& taskId, JsonVariant params) {
346+
// Start async work...
347+
// Call mcp.taskComplete(taskId, resultJson) when done
348+
// Or mcp.taskFail(taskId, errorMessage) on failure
349+
},
350+
mcpd::TaskSupport::Required // Forbidden | Optional | Required
351+
);
352+
```
353+
354+
**TaskSupport levels:**
355+
356+
| Level | Description |
357+
|-------|-------------|
358+
| `Forbidden` | Default. Tool cannot be invoked as a task. |
359+
| `Optional` | Tool can be called normally or as a task. |
360+
| `Required` | Tool *must* be invoked as a task. |
361+
362+
#### Task Lifecycle
363+
364+
```
365+
tools/call (with task field)
366+
367+
368+
Working ──────► Completed (taskComplete)
369+
370+
├──────► Failed (taskFail)
371+
372+
├──────► Cancelled (taskCancel)
373+
374+
└──► InputRequired ──► Working ──► ...
375+
(updateStatus)
376+
```
377+
378+
#### Server-side Task Control
379+
380+
```cpp
381+
// Complete with result
382+
mcp.taskComplete(taskId, R"({"content":[{"type":"text","text":"Done!"}]})");
383+
384+
// Fail with error
385+
mcp.taskFail(taskId, "Download failed: connection timeout");
386+
387+
// Cancel
388+
mcp.taskCancel(taskId);
389+
390+
// Update status (e.g., for input-required flow)
391+
mcp.tasks().updateStatus(taskId, mcpd::TaskStatus::InputRequired, "Need confirmation");
392+
```
393+
394+
#### JSON-RPC Methods
395+
396+
| Method | Description |
397+
|--------|-------------|
398+
| `tasks/get` | Get status of a task by ID |
399+
| `tasks/list` | List all tasks (with pagination) |
400+
| `tasks/result` | Get the result of a completed task |
401+
| `tasks/cancel` | Cancel a running task |
402+
403+
#### Task-Augmented `tools/call`
404+
405+
Clients include a `task` field in the `tools/call` params:
406+
407+
```json
408+
{
409+
"method": "tools/call",
410+
"params": {
411+
"name": "firmware_update",
412+
"arguments": {"url": "https://..."},
413+
"task": {"ttl": 60000}
414+
}
415+
}
416+
```
417+
418+
The response includes a `taskId` instead of the normal tool result.
419+
420+
### Tool Call Hooks
421+
422+
Middleware for logging, access control, or metrics on every tool invocation:
423+
424+
```cpp
425+
// Before hook — can reject the call
426+
mcp.onBeforeToolCall([](const char* toolName, JsonVariant params) -> bool {
427+
Serial.printf("Tool called: %s\n", toolName);
428+
return true; // Return false to reject
429+
});
430+
431+
// After hook — receives timing and error info
432+
mcp.onAfterToolCall([](const mcpd::ToolCallContext& ctx) {
433+
Serial.printf("Tool %s took %lu ms, error: %s\n",
434+
ctx.toolName, ctx.durationMs, ctx.error ? "yes" : "no");
435+
});
436+
```
437+
438+
### Server Instructions & Icons
439+
440+
```cpp
441+
// Guide LLM behavior
442+
mcp.setInstructions("This device monitors a greenhouse. Prefer read-only operations.");
443+
444+
// Server icons (multiple sizes/themes)
445+
mcp.addIcon(MCPIcon("https://example.com/icon.png", "image/png")
446+
.addSize("64x64").setTheme("dark"));
447+
```
448+
322449
### Session Management
323450

324451
- Session ID sent via `Mcp-Session-Id` header

docs/ARCHITECTURE.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# mcpd Architecture
2+
3+
## Overview
4+
5+
mcpd is a single-header-friendly MCP server SDK designed for resource-constrained microcontrollers. The architecture prioritizes low memory usage, zero dynamic allocation where possible, and a clean separation between protocol handling and hardware abstraction.
6+
7+
```
8+
┌─────────────────────────────────────────────┐
9+
│ MCP Clients │
10+
│ (Claude Desktop, Cursor, custom clients) │
11+
└──────────┬──────────┬──────────┬────────────┘
12+
│ HTTP/SSE │ WS │ BLE
13+
▼ ▼ ▼
14+
┌──────────────────────────────────────────────┐
15+
│ Transport Layer │
16+
│ MCPTransport │ MCPTransportWS │ BLETransport│
17+
└──────────────────┬───────────────────────────┘
18+
│ JSON-RPC 2.0
19+
20+
┌──────────────────────────────────────────────┐
21+
│ mcpd::Server │
22+
│ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │
23+
│ │ Dispatch │ │ Sessions │ │ Capabilities │ │
24+
│ │ Router │ │ Manager │ │ Negotiation │ │
25+
│ └────┬─────┘ └──────────┘ └───────────────┘ │
26+
│ │ │
27+
│ ┌────┴──────────────────────────────────┐ │
28+
│ │ Feature Modules │ │
29+
│ │ Tools │ Resources │ Prompts │ Tasks │ │
30+
│ │ Logging │ Sampling │ Elicitation │ │
31+
│ │ Progress │ Roots │ Completion │ │
32+
│ └───────────────────────────────────────┘ │
33+
│ │ │
34+
│ ┌────┴──────────────────────────────────┐ │
35+
│ │ Cross-cutting Concerns │ │
36+
│ │ Auth │ RateLimit │ Metrics │ Hooks │ │
37+
│ └───────────────────────────────────────┘ │
38+
└──────────────────┬───────────────────────────┘
39+
40+
┌──────────────────┴───────────────────────────┐
41+
│ Platform HAL + Built-in Tools │
42+
│ GPIO │ I2C │ SPI │ ADC │ PWM │ UART │ ... │
43+
│ (106 hardware tools across 20+ categories) │
44+
└──────────────────────────────────────────────┘
45+
```
46+
47+
## Core Components
48+
49+
### `mcpd::Server` (`mcpd.h` / `mcpd.cpp`)
50+
51+
The central orchestrator (~1500 lines). Responsibilities:
52+
53+
- **JSON-RPC dispatch**: Routes incoming requests to the correct handler based on `method` field. Supports batch requests (JSON arrays).
54+
- **Capability negotiation**: `initialize` handshake advertises supported features (tools, resources, prompts, logging, sampling, etc.).
55+
- **Session management**: Tracks client sessions with unique IDs, handles `initialized` lifecycle.
56+
- **Notification fan-out**: Pushes `notifications/tools/list_changed`, `notifications/resources/list_changed`, etc. to all active sessions.
57+
58+
### Transport Layer
59+
60+
Three transport implementations, all feeding JSON-RPC strings into the same dispatch:
61+
62+
| Transport | File | Use Case |
63+
|-----------|------|----------|
64+
| **Streamable HTTP + SSE** | `MCPTransport.h` / `MCPTransportSSE.h` | Primary. Claude Desktop via mcpd-bridge. |
65+
| **WebSocket** | `MCPTransportWS.h` | Browser clients, persistent connections. |
66+
| **BLE GATT** | `MCPTransportBLE.h` | Phone apps, proximity-based. Automatic chunking for MTU limits. |
67+
68+
### Feature Modules
69+
70+
Each MCP capability is encapsulated in its own header:
71+
72+
| Module | Header | MCP Method(s) |
73+
|--------|--------|---------------|
74+
| Tools | `MCPTool.h` | `tools/list`, `tools/call` |
75+
| Resources | `MCPResource.h` | `resources/list`, `resources/read`, `resources/subscribe` |
76+
| Resource Templates | `MCPResourceTemplate.h` | `resources/templates/list` |
77+
| Prompts | `MCPPrompt.h` | `prompts/list`, `prompts/get` |
78+
| Tasks | `MCPTask.h` | `tasks/get`, `tasks/list`, `tasks/result`, `tasks/cancel` |
79+
| Logging | `MCPLogging.h` | `logging/setLevel` |
80+
| Sampling | `MCPSampling.h` | `sampling/createMessage` (server→client) |
81+
| Elicitation | `MCPElicitation.h` | `elicitation/create` (server→client) |
82+
| Progress | `MCPProgress.h` | `notifications/progress` |
83+
| Roots | `MCPRoots.h` | `roots/list` |
84+
| Completion | `MCPCompletion.h` | `completion/complete` |
85+
| Content | `MCPContent.h` | Text, Image, Audio, ResourceLink content types |
86+
| Icons | `MCPIcon.h` | Server/tool/resource/prompt icons |
87+
88+
### Cross-cutting Concerns
89+
90+
| Module | Header | Purpose |
91+
|--------|--------|---------|
92+
| Auth | `MCPAuth.h` | API key authentication, multi-key support |
93+
| Rate Limiting | `MCPRateLimit.h` | Token-bucket rate limiter per endpoint |
94+
| Metrics | `MCPMetrics.h` | Prometheus-compatible metrics endpoint |
95+
| Diagnostics | `MCPDiagnostics.h` | Heap monitoring, uptime, request counters |
96+
| Session | `MCPSession.h` | Client session state, subscription tracking |
97+
| Heap | `MCPHeap.h` | Memory-safe allocation tracking for MCU |
98+
99+
### Platform HAL (`src/platform/`)
100+
101+
Hardware abstraction layer isolating platform-specific calls:
102+
103+
- `MCPPlatformESP32.h` — ESP-IDF / Arduino ESP32
104+
- `MCPPlatformRP2040.h` — Raspberry Pi Pico
105+
- `MCPPlatformSTM32.h` — STM32 HAL
106+
- `MCPPlatformGeneric.h` — Fallback for testing
107+
108+
### Built-in Tools (`src/tools/`)
109+
110+
106 pre-built hardware tools organized by category (GPIO, PWM, Servo, I2C, SPI, ADC, UART, CAN, Modbus, LCD, IR, RTC, Camera, ESP-NOW, OTA, etc.). Each tool auto-registers with correct JSON Schema and annotations.
111+
112+
## Design Decisions
113+
114+
### Single-header-friendly
115+
116+
All modules are header-only except `mcpd.cpp`. This means a project can `#include <mcpd.h>` and get everything. The trade-off is compile time, but for MCU projects this is acceptable.
117+
118+
### ArduinoJson for JSON
119+
120+
ArduinoJson v7 is used throughout for JSON parsing and serialization. It provides:
121+
- Static allocation (`StaticJsonDocument`) for predictable memory use
122+
- Streaming serialization to avoid large intermediate buffers
123+
124+
### String over std::string
125+
126+
Arduino `String` is used instead of `std::string` for compatibility across Arduino cores. This is a pragmatic choice — some Arduino cores don't ship a full C++ stdlib.
127+
128+
### No Exceptions
129+
130+
The entire codebase is exception-free. Error handling uses return values (bool, nullptr, error JSON). This is critical for MCU targets where exceptions may not be available or are prohibitively expensive.
131+
132+
### Test Architecture
133+
134+
Tests compile as native C++ (no MCU needed) using mock headers in `test/mock_includes/` that stub out Arduino, WiFi, BLE, and peripheral APIs. The test framework (`test_framework.h`) is custom and minimal — no external dependencies.
135+
136+
```
137+
test/
138+
├── mock_includes/ # Arduino/WiFi/BLE stubs for native compilation
139+
├── native/
140+
│ ├── Makefile # Build and run all test suites
141+
│ └── test_mcp_http.cpp
142+
├── test_framework.h # Custom test macros (TEST, ASSERT_*, RUN_TEST)
143+
├── test_jsonrpc.cpp # JSON-RPC protocol tests
144+
├── test_tools.cpp # Tool/resource/prompt tests
145+
├── test_modules.cpp # Feature module tests
146+
├── test_tasks.cpp # Async tasks tests
147+
└── ... # 14 test files, 1146+ tests total
148+
```
149+
150+
## Message Flow
151+
152+
```
153+
Client mcpd::Server
154+
│ │
155+
│──── POST /mcp ────────────────────│
156+
│ {"jsonrpc":"2.0", │
157+
│ "method":"tools/call", │
158+
│ "params":{...}, │
159+
│ "id":1} │
160+
│ │
161+
│ ┌────────────┤
162+
│ │ Auth check │
163+
│ │ Rate limit │
164+
│ │ Session │
165+
│ │ lookup │
166+
│ │ Before-hook│
167+
│ │ Dispatch │
168+
│ │ After-hook │
169+
│ └────────────┤
170+
│ │
171+
│◄─── 200 OK ──────────────────────│
172+
│ {"jsonrpc":"2.0", │
173+
│ "result":{...}, │
174+
│ "id":1} │
175+
│ │
176+
```
177+
178+
## Memory Budget
179+
180+
Target memory footprint for a typical configuration:
181+
182+
| Component | RAM (approx.) |
183+
|-----------|---------------|
184+
| Server core | ~2 KB |
185+
| Per session | ~200 bytes |
186+
| Per tool registration | ~100 bytes |
187+
| Per resource registration | ~120 bytes |
188+
| JSON parse buffer | 4-16 KB (configurable) |
189+
| HTTP/SSE transport | ~1 KB |
190+
| WebSocket transport | ~2 KB |
191+
| BLE transport | ~3 KB |
192+
| **Typical total** | **~10-20 KB** |
193+
194+
This leaves plenty of room on ESP32 (520 KB SRAM) and RP2040 (264 KB SRAM).

0 commit comments

Comments
 (0)