#ifdef ESP_PLATFORM #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tt::app::mcpsettings { constexpr auto* TAG = "McpSettingsApp"; extern const ::AppManifest manifest; namespace { struct Context { uint32_t appInstanceId; settings::mcp::McpSettings mcpSettings; settings::webserver::WebServerSettings wsSettings; bool updated = false; lv_obj_t* switchMcpEnabled = nullptr; lv_obj_t* labelUrlValue = nullptr; }; void updateUrlDisplay(Context* ctx) { if (ctx->labelUrlValue == nullptr) return; if (!ctx->mcpSettings.mcpEnabled) { lv_label_set_text(ctx->labelUrlValue, "Disabled"); return; } std::string url = "http://"; bool ipAdded = false; for (const char* key : {"WIFI_STA_DEF", "WIFI_AP_DEF"}) { auto* netif = esp_netif_get_handle_from_ifkey(key); if (netif != nullptr) { esp_netif_ip_info_t info; if (esp_netif_get_ip_info(netif, &info) == ESP_OK && info.ip.addr != 0) { char ip[16]; snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip)); url += ip; ipAdded = true; break; } } } if (!ipAdded) url += ctx->wsSettings.wifiMode == settings::webserver::WiFiMode::AccessPoint ? "192.168.4.1" : "Connecting..."; if (url.starts_with("http://") && ctx->wsSettings.webServerPort != 80) url += ":" + std::to_string(ctx->wsSettings.webServerPort); url += "/api/mcp"; lv_label_set_text(ctx->labelUrlValue, url.c_str()); } void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); } void onMcpEnabledSwitch(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); const bool enabled = lv_obj_has_state(ctx->switchMcpEnabled, LV_STATE_CHECKED); getMainDispatcher().dispatch([ctx, enabled] { ctx->mcpSettings.mcpEnabled = enabled; ctx->updated = true; lvgl_lock(); updateUrlDisplay(ctx); lvgl_unlock(); if (!settings::mcp::save(ctx->mcpSettings)) LOG_W(TAG, "Failed to persist MCP settings"); service::webserver::getPubsub()->publish(service::webserver::WebServerEvent::WebServerSettingsChanged); service::webserver::setWebServerEnabled(enabled); }); } void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); ctx->wsSettings = settings::webserver::loadOrGetDefault(); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl_toolbar_create(parent, "MCP Settings"); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); ctx->switchMcpEnabled = lvgl_toolbar_add_switch_action(toolbar); if (ctx->mcpSettings.mcpEnabled) lv_obj_add_state(ctx->switchMcpEnabled, LV_STATE_CHECKED); lv_obj_add_event_cb(ctx->switchMcpEnabled, onMcpEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx); auto* main = lv_obj_create(parent); lv_obj_set_flex_flow(main, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main, LV_PCT(100)); lv_obj_set_flex_grow(main, 1); auto* wrapper = lv_obj_create(main); lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(wrapper, 10, LV_STATE_DEFAULT); lv_obj_set_style_border_width(wrapper, 1, LV_STATE_DEFAULT); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_flex_cross_place(wrapper, LV_FLEX_ALIGN_START, 0); auto* title = lv_label_create(wrapper); lv_label_set_text(title, "MCP Endpoint URL:"); ctx->labelUrlValue = lv_label_create(wrapper); updateUrlDisplay(ctx); auto* info = lv_label_create(main); lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP); lv_obj_set_width(info, LV_PCT(95)); lv_label_set_text(info, "MCP (Model Context Protocol) Screen service allows LLMs to interact with the device screen, audio, and tools directly.\n\nEndpoints:\n- POST /api/mcp (JSON-RPC tools)\n- POST /api/screen/raw (big-endian RGB565 writes)\n\nTo show the LLM canvas, select 'MCP Screen' in Settings -> Display -> Screensaver. The canvas also pops up automatically when an LLM sends a draw command."); } int32_t appMain(int, char**) { Context ctx{}; ctx.appInstanceId = app_scheduler_current_app_id(); ctx.mcpSettings = settings::mcp::loadOrGetDefault(); TaskEventGroup group{}; task_event_group_construct(&group); AppEventSubscription sub{}; check(app_event_subscribe(&sub, &group) == ERROR_NONE); auto window = window_manager_create(ctx.appInstanceId, createWidgets, &ctx); bool close = false; while (!close) { task_event_group_wait_any(&group, nullptr, portMAX_DELAY); AppEvent event{}; while (app_event_poll(&sub, &event) == ERROR_NONE) if (event.type == APP_EVENT_CLOSE) { close = true; break; } } window_manager_remove(window); check(app_event_unsubscribe(&sub) == ERROR_NONE); task_event_group_destruct(&group); return 0; } } extern const ::AppManifest manifest = { .id = "McpSettings", .name = "MCP Screen", .category = APP_CATEGORY_SETTINGS, .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) }, .flags = 0, .stack = { .depth = 8192, .desired_memory_capability = 0 } }; } #endif