Enable MCP tools in POSIX simulator
This commit is contained in:
@@ -83,8 +83,8 @@ private:
|
|||||||
static error_t handleApiScreenshot(struct HttpServerRequest* request, void* user_ctx);
|
static error_t handleApiScreenshot(struct HttpServerRequest* request, void* user_ctx);
|
||||||
static error_t handleApiSimTouch(struct HttpServerRequest* request, void* user_ctx);
|
static error_t handleApiSimTouch(struct HttpServerRequest* request, void* user_ctx);
|
||||||
static error_t handleSimViewer(struct HttpServerRequest* request, void* user_ctx);
|
static error_t handleSimViewer(struct HttpServerRequest* request, void* user_ctx);
|
||||||
#ifdef ESP_PLATFORM
|
|
||||||
static error_t handleApiMcp(struct HttpServerRequest* request, void* user_ctx);
|
static error_t handleApiMcp(struct HttpServerRequest* request, void* user_ctx);
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
static error_t handleApiScreenRaw(struct HttpServerRequest* request, void* user_ctx);
|
static error_t handleApiScreenRaw(struct HttpServerRequest* request, void* user_ctx);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ namespace app {
|
|||||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||||
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
||||||
#endif
|
#endif
|
||||||
|
#else
|
||||||
|
namespace mcpsettings { extern const ::AppManifest manifest; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace trackballsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
namespace trackballsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
||||||
@@ -252,12 +254,14 @@ static void registerInternalApps() {
|
|||||||
app_manager_add(&app::development::manifest);
|
app_manager_add(&app::development::manifest);
|
||||||
app_manager_add(&app::webserversettings::manifest);
|
app_manager_add(&app::webserversettings::manifest);
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
|
app_manager_add(&app::mcpsettings::manifest);
|
||||||
app_manager_add(&app::apwebserver::manifest);
|
app_manager_add(&app::apwebserver::manifest);
|
||||||
app_manager_add(&app::crashdiagnostics::manifest);
|
app_manager_add(&app::crashdiagnostics::manifest);
|
||||||
app_manager_add(&app::mcpsettings::manifest);
|
|
||||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||||
app_manager_add(&app::keyboardsettings::manifest);
|
app_manager_add(&app::keyboardsettings::manifest);
|
||||||
#endif
|
#endif
|
||||||
|
#else
|
||||||
|
app_manager_add(&app::mcpsettings::manifest);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (device_exists_of_type(&TRACKBALL_TYPE)) {
|
if (device_exists_of_type(&TRACKBALL_TYPE)) {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#ifndef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <app/event.h>
|
||||||
|
#include <app/manifest.h>
|
||||||
|
#include <app/scheduler.h>
|
||||||
|
#include <lvgl/lvgl.h>
|
||||||
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
|
namespace tt::app::mcpsettings {
|
||||||
|
struct Context { AppInstanceId instanceId; };
|
||||||
|
|
||||||
|
void createMcpInfo(lv_obj_t* parent, void* userData) {
|
||||||
|
auto* context = static_cast<Context*>(userData);
|
||||||
|
lv_obj_t* title = lv_label_create(parent);
|
||||||
|
lv_label_set_text(title, "MCP Settings");
|
||||||
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18);
|
||||||
|
|
||||||
|
lv_obj_t* info = lv_label_create(parent);
|
||||||
|
lv_label_set_text(info,
|
||||||
|
"MCP is available while the simulator web server is running.\n\n"
|
||||||
|
"Endpoint: POST /api/mcp\n"
|
||||||
|
"JSON-RPC methods: tools/list, tools/call\n\n"
|
||||||
|
"Simulator tools: get_capabilities, list_apps, run_app\n\n"
|
||||||
|
"Open the simulator dashboard/API at http://127.0.0.1/api/sysinfo.");
|
||||||
|
lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_width(info, LV_PCT(90));
|
||||||
|
lv_obj_align(info, LV_ALIGN_CENTER, 0, 8);
|
||||||
|
|
||||||
|
(void)context;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t appMain(int, char**) {
|
||||||
|
Context context{app_scheduler_current_app_id()};
|
||||||
|
TaskEventGroup group{};
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
AppEventSubscription subscription{};
|
||||||
|
if (app_event_subscribe(&subscription, &group) != ERROR_NONE) {
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
auto window = window_manager_create(context.instanceId, createMcpInfo, &context);
|
||||||
|
bool close = false;
|
||||||
|
while (!close) {
|
||||||
|
task_event_group_wait_any(&group, nullptr, portMAX_DELAY);
|
||||||
|
AppEvent event{};
|
||||||
|
while (app_event_poll(&subscription, &event) == ERROR_NONE) {
|
||||||
|
if (event.type == APP_EVENT_CLOSE) { close = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window_manager_remove(window);
|
||||||
|
app_event_unsubscribe(&subscription);
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern const AppManifest manifest = {
|
||||||
|
.id = "McpSettings",
|
||||||
|
.name = "MCP Settings",
|
||||||
|
.category = APP_CATEGORY_SETTINGS,
|
||||||
|
.location = {APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain)},
|
||||||
|
.flags = 0,
|
||||||
|
.stack = {.depth = 8192, .desired_memory_capability = 0}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#ifndef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <Tactility/service/webserver/WebServerService.h>
|
||||||
|
#include <app/manager.h>
|
||||||
|
#include <app/start.h>
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <http/server.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tt::service::webserver {
|
||||||
|
namespace {
|
||||||
|
constexpr size_t JSON_BODY_LIMIT = 64 * 1024;
|
||||||
|
|
||||||
|
cJSON* makeError(const cJSON* id, int code, const char* message) {
|
||||||
|
cJSON* response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
|
||||||
|
cJSON* error = cJSON_AddObjectToObject(response, "error");
|
||||||
|
cJSON_AddNumberToObject(error, "code", code);
|
||||||
|
cJSON_AddStringToObject(error, "message", message);
|
||||||
|
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
|
||||||
|
else cJSON_AddNullToObject(response, "id");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* makeTextResult(const cJSON* id, const char* text, bool isError = false) {
|
||||||
|
cJSON* response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
|
||||||
|
cJSON* result = cJSON_AddObjectToObject(response, "result");
|
||||||
|
cJSON* content = cJSON_AddArrayToObject(result, "content");
|
||||||
|
cJSON* item = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(item, "type", "text");
|
||||||
|
cJSON_AddStringToObject(item, "text", text);
|
||||||
|
cJSON_AddItemToArray(content, item);
|
||||||
|
if (isError) cJSON_AddBoolToObject(result, "isError", true);
|
||||||
|
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
|
||||||
|
else cJSON_AddNullToObject(response, "id");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* makeToolList(const cJSON* id) {
|
||||||
|
cJSON* response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
|
||||||
|
cJSON* result = cJSON_AddObjectToObject(response, "result");
|
||||||
|
cJSON* tools = cJSON_AddArrayToObject(result, "tools");
|
||||||
|
|
||||||
|
cJSON* capabilities = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(capabilities, "name", "get_capabilities");
|
||||||
|
cJSON_AddStringToObject(capabilities, "description", "Get the simulator platform and display capability information.");
|
||||||
|
cJSON_AddItemToObject(capabilities, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{}}"));
|
||||||
|
cJSON_AddItemToArray(tools, capabilities);
|
||||||
|
|
||||||
|
cJSON* listApps = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(listApps, "name", "list_apps");
|
||||||
|
cJSON_AddStringToObject(listApps, "description", "List apps available in the simulator.");
|
||||||
|
cJSON_AddItemToObject(listApps, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{}}"));
|
||||||
|
cJSON_AddItemToArray(tools, listApps);
|
||||||
|
|
||||||
|
cJSON* runApp = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(runApp, "name", "run_app");
|
||||||
|
cJSON_AddStringToObject(runApp, "description", "Launch an installed simulator app by its app ID.");
|
||||||
|
cJSON_AddItemToObject(runApp, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{\"app_id\":{\"type\":\"string\"}},\"required\":[\"app_id\"]}"));
|
||||||
|
cJSON_AddItemToArray(tools, runApp);
|
||||||
|
|
||||||
|
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
|
||||||
|
else cJSON_AddNullToObject(response, "id");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string listAppsJson() {
|
||||||
|
std::vector<const AppManifest*> manifests;
|
||||||
|
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
|
||||||
|
static_cast<std::vector<const AppManifest*>*>(context)->push_back(manifest);
|
||||||
|
}, &manifests);
|
||||||
|
|
||||||
|
cJSON* apps = cJSON_CreateArray();
|
||||||
|
for (const AppManifest* manifest : manifests) {
|
||||||
|
cJSON* app = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(app, "id", manifest->id);
|
||||||
|
cJSON_AddStringToObject(app, "name", manifest->name);
|
||||||
|
cJSON_AddItemToArray(apps, app);
|
||||||
|
}
|
||||||
|
char* text = cJSON_PrintUnformatted(apps);
|
||||||
|
std::string output = text == nullptr ? "[]" : text;
|
||||||
|
cJSON_free(text);
|
||||||
|
cJSON_Delete(apps);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* handleCall(const cJSON* id, const cJSON* params) {
|
||||||
|
const cJSON* name = cJSON_GetObjectItemCaseSensitive(params, "name");
|
||||||
|
const cJSON* args = cJSON_GetObjectItemCaseSensitive(params, "arguments");
|
||||||
|
if (!cJSON_IsString(name) || (args != nullptr && !cJSON_IsObject(args))) {
|
||||||
|
return makeError(id, -32602, "Invalid params");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name->valuestring, "get_capabilities") == 0) {
|
||||||
|
return makeTextResult(id, "{\"platform\":\"tactility-simulator\",\"mcp\":true}");
|
||||||
|
}
|
||||||
|
if (strcmp(name->valuestring, "list_apps") == 0) {
|
||||||
|
const std::string apps = listAppsJson();
|
||||||
|
return makeTextResult(id, apps.c_str());
|
||||||
|
}
|
||||||
|
if (strcmp(name->valuestring, "run_app") == 0) {
|
||||||
|
const cJSON* appId = cJSON_GetObjectItemCaseSensitive(args, "app_id");
|
||||||
|
if (!cJSON_IsString(appId) || appId->valuestring == nullptr || appId->valuestring[0] == '\0') {
|
||||||
|
return makeError(id, -32602, "app_id is required");
|
||||||
|
}
|
||||||
|
AppManifest manifest{};
|
||||||
|
if (app_manager_find_manifest(appId->valuestring, &manifest) != ERROR_NONE) {
|
||||||
|
return makeTextResult(id, "app not found", true);
|
||||||
|
}
|
||||||
|
AppInstanceId instanceId = 0;
|
||||||
|
const error_t result = app_start(appId->valuestring, 0, nullptr, &instanceId);
|
||||||
|
if (result != ERROR_NONE) return makeTextResult(id, "app launch failed", true);
|
||||||
|
return makeTextResult(id, "ok");
|
||||||
|
}
|
||||||
|
return makeError(id, -32602, "Unknown simulator MCP tool");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* handleRpc(const char* body) {
|
||||||
|
cJSON* request = cJSON_Parse(body);
|
||||||
|
if (request == nullptr) return makeError(nullptr, -32700, "Parse error");
|
||||||
|
const cJSON* id = cJSON_GetObjectItemCaseSensitive(request, "id");
|
||||||
|
const cJSON* version = cJSON_GetObjectItemCaseSensitive(request, "jsonrpc");
|
||||||
|
const cJSON* method = cJSON_GetObjectItemCaseSensitive(request, "method");
|
||||||
|
const cJSON* params = cJSON_GetObjectItemCaseSensitive(request, "params");
|
||||||
|
|
||||||
|
cJSON* response = nullptr;
|
||||||
|
if (!cJSON_IsObject(request) || !cJSON_IsString(version) || strcmp(version->valuestring, "2.0") != 0 || !cJSON_IsString(method)) {
|
||||||
|
response = makeError(id, -32600, "Invalid Request");
|
||||||
|
} else if (strcmp(method->valuestring, "tools/list") == 0) {
|
||||||
|
response = makeToolList(id);
|
||||||
|
} else if (strcmp(method->valuestring, "tools/call") == 0) {
|
||||||
|
response = handleCall(id, params);
|
||||||
|
} else {
|
||||||
|
response = makeError(id, -32601, "Method not found");
|
||||||
|
}
|
||||||
|
cJSON_Delete(request);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t WebServerService::handleApiMcp(HttpServerRequest* request, void*) {
|
||||||
|
const uint64_t length = http_server_request_get_content_length(request);
|
||||||
|
if (length == 0 || length > JSON_BODY_LIMIT) {
|
||||||
|
http_server_request_send_error(request, 400, "Invalid Content-Length");
|
||||||
|
return ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
std::string body(static_cast<size_t>(length), '\0');
|
||||||
|
size_t received = 0;
|
||||||
|
while (received < body.size()) {
|
||||||
|
const int count = http_server_request_receive(request, body.data() + received, body.size() - received);
|
||||||
|
if (count <= 0) {
|
||||||
|
http_server_request_send_error(request, 400, "Incomplete request body");
|
||||||
|
return ERROR_UNDEFINED;
|
||||||
|
}
|
||||||
|
received += static_cast<size_t>(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* response = handleRpc(body.c_str());
|
||||||
|
char* text = cJSON_PrintUnformatted(response);
|
||||||
|
cJSON_Delete(response);
|
||||||
|
if (text == nullptr) {
|
||||||
|
http_server_request_send_error(request, 500, "Serialization failed");
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
http_server_request_set_content_type(request, "application/json");
|
||||||
|
http_server_request_send_string(request, text);
|
||||||
|
cJSON_free(text);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
@@ -507,14 +507,14 @@ bool WebServerService::startServer() {
|
|||||||
.callback = handleAdminPost,
|
.callback = handleAdminPost,
|
||||||
.user_ctx = ctx
|
.user_ctx = ctx
|
||||||
},
|
},
|
||||||
#ifdef ESP_PLATFORM
|
// MCP is available on physical devices and the POSIX simulator.
|
||||||
// MCP is LAN-local and is enabled whenever the web server is enabled.
|
|
||||||
{
|
{
|
||||||
.uri = "/api/mcp",
|
.uri = "/api/mcp",
|
||||||
.method = HTTP_METHOD_POST,
|
.method = HTTP_METHOD_POST,
|
||||||
.callback = handleApiMcp,
|
.callback = handleApiMcp,
|
||||||
.user_ctx = ctx
|
.user_ctx = ctx
|
||||||
},
|
},
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
{
|
{
|
||||||
.uri = "/api/screen/raw",
|
.uri = "/api/screen/raw",
|
||||||
.method = HTTP_METHOD_POST,
|
.method = HTTP_METHOD_POST,
|
||||||
|
|||||||
Reference in New Issue
Block a user