12 KiB
McpScreen for Tactility — Implementation Plan
Goal
Port the hardware-agnostic MCP Screen design from:
/Users/adolforeyna/Projects/MicroPython/test1/Screen
into a native Tactility application while preserving compatibility with the existing host-side MCP bridge and Hermes voice service.
The original reference documents and implementations are:
system_design_specs.mdmcp_server.pymcp_bridge.pymain.pyvideo_stream.pyvoice_assistant.pylib/audio_util.py
Target application
- Source folder:
Apps/McpScreen - App ID:
one.tactility.mcpscreen - Initial platform: ESP32-S3
- Initial Tactility SDK:
0.7.0-dev - Language: C/C++ with ESP-IDF, FreeRTOS, Tactility SDK, and LVGL
Current implementation status
Phase 1 is implemented, builds successfully for ESP32-S3, and has been tested on a 320x240 Tactility device at runtime.
Completed:
- Tactility app lifecycle and app-owned drawing area
- HTTP JSON-RPC endpoint on port 80
tools/listget_capabilitiesclear_screendraw_text- request-size limits and JSON-RPC error responses
- live
tools/listandget_capabilitiescalls - live
clear_screenanddraw_textcalls - live malformed-JSON error handling
- crash-safe HTTP worker shutdown during app destruction
- three forced stop/start cycles with the management and MCP endpoints still responsive afterward
Blocked by the current external-app ABI:
- UDP discovery requires
lwip_recvfromandlwip_sendto, which the running Tactility 0.7.0-dev firmware does not export to ELF apps. The existing bridge falls back to HTTP subnet scanning, so MCP connectivity remains available.
Pending verification:
- verify service behavior while the app is genuinely hidden behind another
running application. A remote HelloWorld launch did not trigger
onHide, so this needs a manual launcher/device interaction test.
Shutdown implementation note
Tactility unloads external-app ELF memory immediately after onDestroy
returns. An app task blocked in accept() therefore cannot be allowed to
survive destruction. The HTTP listener is non-blocking and client reads have
short timeouts. onHide only clears the run flag and returns immediately so
it does not block Tactility's GUI lifecycle or close an lwIP descriptor from
the wrong task. The worker owns the socket shutdown, then suspends itself
asynchronously. The next onShow reaps it before starting a new worker, while
onDestroy waits for and deletes it before returning.
Design principles
- Preserve the existing MCP tool names and JSON-RPC contract wherever the underlying Tactility hardware supports them.
- Keep network, audio, and long-running work outside the LVGL task.
- Perform all LVGL operations under the Tactility LVGL lock or dispatch them onto the LVGL task.
- Use Tactility device and HAL interfaces instead of board-specific GPIO assumptions wherever possible.
- Return structured unsupported-capability errors instead of silently emulating unavailable hardware.
- Treat files supplied over MCP as untrusted input and constrain file access to the application's user-data directory.
- Do not port
execute_pythonliterally. Native firmware must not expose arbitrary code execution.
Proposed application structure
Apps/McpScreen/
├── CMakeLists.txt
├── manifest.properties
├── IMPLEMENTATION_PLAN.md
└── main/
├── CMakeLists.txt
└── Source/
├── main.cpp
├── McpScreenApp.cpp
├── McpScreenApp.h
├── McpServer.cpp
├── McpServer.h
├── DiscoveryService.cpp
├── DiscoveryService.h
├── ToolRegistry.cpp
├── ToolRegistry.h
├── ToolDispatcher.cpp
├── ToolDispatcher.h
├── DisplayTools.cpp
├── DisplayTools.h
├── AudioTools.cpp
├── AudioTools.h
├── SystemTools.cpp
├── SystemTools.h
├── DashboardView.cpp
└── DashboardView.h
The file boundaries may be consolidated during the first slice if a smaller implementation is easier to validate.
Runtime architecture
Application lifecycle
onCreate- Allocate application state.
onShow- Build the dashboard and MCP-controlled display canvas.
- Attach the current UI objects to the shared application state.
- Start the HTTP MCP service.
onHide- Signal the HTTP MCP service to stop without blocking the GUI lifecycle.
- Detach and invalidate UI object pointers.
onDestroy- Join/delete any remaining worker and close sockets as a safety fallback.
- Release buffers, devices, and application state.
The MCP server is deliberately foreground-only. Tactility does not need to keep the listener or display-control task active while another app owns the screen.
FreeRTOS responsibilities
- MCP HTTP task
- Listen for HTTP requests.
- Parse JSON-RPC requests.
- Dispatch tools.
- Serialize JSON-RPC responses.
- UDP discovery task
- Bind UDP port 5000.
- Reply to discovery packets.
- Display work
- Marshal changes through the LVGL lock or an LVGL async callback.
- Audio tasks
- Record and play I2S data without blocking the UI or MCP listener.
- Future streaming tasks
- Own TCP/UDP frame sockets and preallocated frame buffers.
Network compatibility
UDP discovery
- Port:
5000 - Request: exact bytes
DISCOVER_SCREEN - Response: exact bytes
SCREEN_IP_80
MCP HTTP endpoint
- Port:
80 - Method and path:
POST /api/mcp - Content type:
application/json - Protocol: JSON-RPC 2.0
- Required methods:
tools/listtools/call
Raw display endpoint
Added after the basic MCP slice:
- Method and path:
POST /api/screen/raw - Query parameters:
x,y,w,h - Body: raw big-endian RGB565 bytes
MCP response behavior
Successful tool calls retain the existing shape:
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "..."
}
]
},
"id": 1
}
Errors use JSON-RPC error objects:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "..."
},
"id": 1
}
Phase 1 — MCP foundation
Scope
- Scaffold
Apps/McpScreen. - Add a status/dashboard view showing:
- Wi-Fi state
- server state
- port
- last tool
- last error
- Start the HTTP server on port 80.
- Implement
POST /api/mcp. - Implement JSON-RPC request validation and error handling.
- Implement
tools/list. - Implement UDP discovery on port 5000.
- Implement the first three tools:
get_capabilitiesclear_screendraw_text
- Test with the existing
mcp_bridge.py.
Initial tool semantics
get_capabilities
Return live display information:
{
"color": true,
"width": 320,
"height": 240,
"formats": ["rgb565_base64", "bmp_base64"],
"platform": "tactility",
"appVersion": "0.1.0"
}
Width, height, and color format must be queried at runtime.
clear_screen
- Accept
colorvalues compatible with the original API:0: white1: black
- Mark MCP display override active.
- Clear the app-owned canvas/display area.
draw_text
- Accept
text,x,y, and optionalsize. - Mark MCP display override active.
- Render inside the app-owned canvas.
- Clamp coordinates to the drawable area.
Phase 1 acceptance criteria
- The app builds and packages for ESP32-S3.
- Opening the app displays its server status.
- UDP discovery returns
SCREEN_IP_80. - The existing bridge can discover the device.
tools/listreturns valid schemas for the three initial tools.get_capabilitiesreports the actual Tactility display dimensions.clear_screenanddraw_textvisibly update the app.- Malformed JSON and unknown methods return JSON-RPC errors without crashing.
- Repeated requests do not leak tasks, sockets, or request buffers.
- App hiding/showing behavior is documented from a device test.
Phase 2 — Display tools
Add:
draw_raw_rgb565POST /api/screen/rawdraw_color_bmpdraw_imageget_screenshotset_backlightset_screen_power
Display commands should target an app-owned LVGL canvas or image buffer. Direct panel access should only be used when the Tactility display HAL guarantees safe ownership and synchronization.
Image preprocessing may remain in mcp_bridge.py initially. That avoids large
PNG/JPEG decoders and unnecessary memory pressure on the ESP32-S3.
Phase 3 — Audio tools
Port the working implementation from Apps/AudioTest:
play_tonerecord_voiceplay_audioplay_audio_base64
Audio baseline:
- 16 kHz
- 16-bit signed PCM
- mono
- I2S device discovered through
device_find_by_name("i2s0")
Recordings should be streamed to the application's user-data directory instead of requiring one large fixed-duration RAM allocation.
Phase 4 — Hardware and system tools
Add where supported:
get_touchget_batteryset_ledget_sensorsscan_blesync_timeread_filewrite_filedownload_file
File operations must:
- resolve paths under the app user-data directory;
- reject absolute paths and traversal such as
../; - enforce practical request and file-size limits.
execute_python is intentionally excluded. A future allow-listed diagnostic
command tool may replace it if needed.
Phase 5 — Video streaming
Add:
- TCP frame server
- UDP chunked frame server
- stream activity timeout
get_video_streaming_instructionsget_stream_stats
The new protocol should advertise the actual Tactility display dimensions and native color format. The old RLCD-specific 15,000-byte monochrome mapping may be offered only as an optional compatibility mode.
Use preallocated buffers and avoid per-frame heap allocation.
Phase 6 — Dashboard and Hermes voice assistant
Add:
- five-second dashboard refresh;
- MCP override mode;
- an explicit local action to leave override mode;
- Hermes WebSocket client;
- touch-to-talk;
- 16 kHz, 16-bit mono microphone streaming;
- incoming PCM/WAV playback;
- transcript, thinking, listening, and speaking UI states.
Compatibility notes
- Keep the existing host bridge usable throughout development.
- Preserve tool names and argument names unless a compatibility defect is documented.
draw_imagecan continue to be host-preprocessed into PBM or RGB565.- Screenshot output may change from PBM to PNG/RGB565 if the bridge is updated to understand both.
- Hardware-specific tools should report support through
get_capabilities.
Security and robustness limits
The first implementation should define conservative limits for:
- HTTP header size
- JSON body size
- base64 payload size
- socket read timeout
- simultaneous clients
- filename length
- file size
- text length
- drawing bounds
- audio duration
Only one display mutation should run at a time. Audio operations should also be serialized around the shared I2S device.
Build workflow
From the TactilityApps repository:
. /Users/adolforeyna/esp/esp-idf/export.sh
export TACTILITY_SDK_PATH=/Users/adolforeyna/.gemini/antigravity/scratch/tactility/release/TactilitySDK
python3 tactility.py Apps/McpScreen build esp32s3 --local-sdk
Immediate next task
Implement Phase 1 as a minimal vertical slice:
- Scaffold the app and lifecycle.
- Add a small server-status UI.
- Add UDP discovery.
- Add HTTP and JSON-RPC parsing.
- Add
tools/list. - Add
get_capabilities,clear_screen, anddraw_text. - Build.
- Run a host smoke test through the existing bridge.