feat: integrate MCP server and screen override into system firmware

- Add McpSettings (load/save mcpEnabled, mcpOverrideEnabled to /data/service/mcp/settings.properties)
- Add McpSystem: global state, canvas drawing (RGB565, BMP, PBM, text), I2S audio (WAV, MP3 via minimp3, tone, record)
- Add McpHandler: unauthenticated POST /api/mcp (JSON-RPC 2.0, 13 tools) and POST /api/screen/raw endpoints
- Route MCP endpoints before Basic Auth check in WebServerService::handleApiPost
- Start HTTP server at boot if either webServerEnabled OR mcpEnabled is set
- Fix setEnabled: start unconditionally when called with true; only stop if both WS and MCP are disabled
- Add McpSettingsApp (Settings category): toggle MCP service + screen override, show live endpoint URL
- Add McpOverrideApp (appId one.tactility.mcpscreen): full-screen LVGL canvas for LLM-driven display override
- Register both new apps in Tactility.cpp
This commit is contained in:
Adolfo Reyna
2026-06-26 23:02:09 -04:00
parent 28ff01561a
commit 41004a3c6f
11 changed files with 4215 additions and 5 deletions
@@ -5,6 +5,7 @@
#include <Tactility/service/webserver/AssetVersion.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/settings/WebServerSettings.h>
#include <Tactility/settings/McpSettings.h>
#include <Tactility/MountPoints.h>
#include <Tactility/file/File.h>
#include <Tactility/Logger.h>
@@ -222,7 +223,9 @@ bool WebServerService::onStart(ServiceContext& service) {
lock.lock();
g_cachedSettings = settings::webserver::loadOrGetDefault();
g_settingsCached = true;
serverEnabled = g_cachedSettings.webServerEnabled;
auto mcpSettings = settings::mcp::loadOrGetDefault();
serverEnabled = g_cachedSettings.webServerEnabled || mcpSettings.mcpEnabled;
}
// Subscribe to settings change events to refresh cache
settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) {
@@ -236,10 +239,10 @@ bool WebServerService::onStart(ServiceContext& service) {
// Start HTTP server only if enabled in settings (default: OFF to save memory)
if (serverEnabled) {
LOGGER.info("WebServer enabled in settings, starting HTTP server...");
LOGGER.info("WebServer or MCP enabled in settings, starting HTTP server...");
setEnabled(true);
} else {
LOGGER.info("WebServer disabled in settings, NOT starting HTTP server (saves ~10KB RAM)");
LOGGER.info("WebServer and MCP disabled in settings, NOT starting HTTP server (saves RAM)");
setEnabled(false);
}
@@ -268,11 +271,16 @@ void WebServerService::setEnabled(bool enabled) {
lock.lock();
if (enabled) {
// Start unconditionally if explicitly requested
if (!httpServer || !httpServer->isStarted()) {
startServer();
}
} else {
if (httpServer && httpServer->isStarted()) {
// Stop only if both web server and MCP are disabled
auto wsSettings = settings::webserver::loadOrGetDefault();
auto mcpSettings = settings::mcp::loadOrGetDefault();
bool anyEnabled = wsSettings.webServerEnabled || mcpSettings.mcpEnabled;
if (!anyEnabled && httpServer && httpServer->isStarted()) {
stopServer();
}
}
@@ -1035,13 +1043,22 @@ esp_err_t WebServerService::handleApiGet(httpd_req_t* request) {
// API POST dispatcher - all POST endpoints require authentication
esp_err_t WebServerService::handleApiPost(httpd_req_t* request) {
const char* uri = request->uri;
// Route MCP endpoints unauthenticated
if (strncmp(uri, "/api/mcp", 8) == 0) {
return handleApiMcp(request);
}
if (strncmp(uri, "/api/screen/raw", 15) == 0) {
return handleApiScreenRaw(request);
}
bool authPassed = false;
esp_err_t authResult = validateRequestAuth(request, authPassed);
if (!authPassed) {
return authResult;
}
const char* uri = request->uri;
if (strncmp(uri, "/api/apps/run", 13) == 0) {
return handleApiAppsRun(request);
}