En este post tenéis el código necesario para lanzar un un MCP en Arduino UNU R4 WiFi que permita a un LLM mostrar diferentes imágenes pre-configuradas en la matriz de leds, para más información puedes ver el vídeo:
/*
Servidor MCP mínimo para Arduino UNO R4 WiFi.
Dependencias:
- WiFiS3 (incluida con el core Arduino Renesas)
- Arduino_LED_Matrix (incluida con el core Arduino Renesas)
- ArduinoJson 7.x (instalar desde el gestor de bibliotecas)
Transporte MCP: Streamable HTTP, respuestas JSON (sin SSE), endpoint /mcp.
Las dos funciones de hardware están vacías intencionadamente.
*/
#include <WiFiS3.h>
#include <Arduino_LED_Matrix.h>
#include <ArduinoJson.h>
#define DEBUG 1
#if DEBUG
#define DEBUG_MSG(x) Serial.print(x)
#define DEBUG_BR() Serial.println()
#else
#define DEBUG_MSG(x) ((void)0)
#define DEBUG_BR() ((void)0)
#endif
// ---------- Configuración WiFi ----------
const char WIFI_SSID[] = "WIFI_NAME";
const char WIFI_PASSWORD[] = "WIFI_PASSWORD";
// Ajusta estas constantes a la red local donde se conectará la placa.
const IPAddress MCP_IP(192, 168, 1, 15);
const uint16_t MCP_PORT = 8080;
const IPAddress MCP_DNS(192, 168, 1, 1);
const IPAddress MCP_GATEWAY(192, 168, 1, 1);
const IPAddress MCP_SUBNET(255, 255, 255, 0);
const char MCP_PROTOCOL_VERSION[] = "2025-06-18";
const char MCP_PATH[] = "/mcp";
const size_t MAX_HTTP_BODY = 4096;
const unsigned long CLIENT_TIMEOUT_MS = 3000;
// Estados admitidos por la pantalla.
static const char* const VALID_STATES[] = {
"alegre", "triste", "enfado", "corazon","risa", "si", "no", "ok", "ko"
};
static const size_t VALID_STATES_COUNT =
sizeof(VALID_STATES) / sizeof(VALID_STATES[0]);
WiFiServer mcpServer(MCP_PORT);
ArduinoLEDMatrix matrix;
const uint32_t EMPTY_FRAME[3] = {0x00000000, 0x00000000, 0x00000000};
byte OK_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
byte HAPPY_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
byte SAD_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 }
};
byte KO_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0 },
{ 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0 },
{ 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
byte YES_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
byte NO_FRAME[8][12] = {
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 }
};
byte XD_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0 },
{ 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0 },
{ 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0 },
{ 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
byte HEART_FRAME[8][12] = {
{ 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }
};
byte ANGRY_FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
//Plantilla vacia
/*byte _FRAME[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};*/
// ---------- Funciones que debe completar el usuario ----------
void pantalla(const char* estado) {
if (strcmp(estado, "alegre") == 0) {
// Mostrar cara alegre
matrix.renderBitmap(HAPPY_FRAME, 8, 12);
}
else if (strcmp(estado, "triste") == 0) {
// Mostrar cara triste
matrix.renderBitmap(SAD_FRAME, 8, 12);
}
else if (strcmp(estado, "risa") == 0) {
// Mostrar cara riendo
matrix.renderBitmap(XD_FRAME, 8, 12);
}
else if (strcmp(estado, "si") == 0) {
// Mostrar símbolo sí
matrix.renderBitmap(YES_FRAME, 8, 12);
}
else if (strcmp(estado, "no") == 0) {
// Mostrar símbolo no
matrix.renderBitmap(NO_FRAME, 8, 12);
}
else if (strcmp(estado, "ok") == 0) {
// Mostrar OK
matrix.renderBitmap(OK_FRAME, 8, 12);
}
else if (strcmp(estado, "ko") == 0) {
// Mostrar KO
matrix.renderBitmap(KO_FRAME, 8, 12);
}
else if (strcmp(estado, "corazon") == 0) {
// Mostrar KO
matrix.renderBitmap(HEART_FRAME, 8, 12);
}
else if (strcmp(estado, "enfado") == 0) {
// Mostrar KO
matrix.renderBitmap(ANGRY_FRAME, 8, 12);
}
}
void ventilador(uint8_t velocidad) {
// Intencionadamente vacía.
// Aquí se puede convertir 0..100 al PWM o al control requerido por el motor.
}
// ---------- Utilidades HTTP ----------
void sendCorsHeaders(WiFiClient& client) {
client.println("Access-Control-Allow-Origin: *");
client.println("Access-Control-Allow-Methods: POST, GET, OPTIONS");
client.println("Access-Control-Allow-Headers: Content-Type, Accept, MCP-Protocol-Version");
client.println("Access-Control-Expose-Headers: MCP-Protocol-Version");
client.println("Access-Control-Max-Age: 86400");
}
void sendEmptyResponse(WiFiClient& client, int statusCode, const char* statusText) {
client.print("HTTP/1.1 ");
client.print(statusCode);
client.print(' ');
client.println(statusText);
client.println("Content-Length: 0");
sendCorsHeaders(client);
client.println("Connection: close");
client.println();
}
void sendJsonResponse(WiFiClient& client, int statusCode, const char* statusText,
JsonDocument& response) {
const size_t length = measureJson(response);
client.print("HTTP/1.1 ");
client.print(statusCode);
client.print(' ');
client.println(statusText);
client.println("Content-Type: application/json; charset=utf-8");
client.print("MCP-Protocol-Version: ");
client.println(MCP_PROTOCOL_VERSION);
client.print("Content-Length: ");
client.println(length);
sendCorsHeaders(client);
client.println("Connection: close");
client.println();
serializeJson(response, client);
}
void setResponseId(JsonDocument& response, JsonVariantConst id) {
if (id.is<const char*>()) {
response["id"] = id.as<const char*>();
} else if (id.is<long>()) {
response["id"] = id.as<long>();
} else if (id.is<unsigned long>()) {
response["id"] = id.as<unsigned long>();
} else {
response["id"] = nullptr;
}
}
void makeError(JsonDocument& response, JsonVariantConst id, int code,
const char* message) {
response.clear();
response["jsonrpc"] = "2.0";
setResponseId(response, id);
response["error"]["code"] = code;
response["error"]["message"] = message;
}
void makeToolError(JsonDocument& response, JsonVariantConst id,
const char* message) {
response.clear();
response["jsonrpc"] = "2.0";
setResponseId(response, id);
response["result"]["isError"] = true;
JsonObject item = response["result"]["content"].add<JsonObject>();
item["type"] = "text";
item["text"] = message;
}
void makeToolSuccess(JsonDocument& response, JsonVariantConst id,
const char* message) {
response.clear();
response["jsonrpc"] = "2.0";
setResponseId(response, id);
response["result"]["isError"] = false;
JsonObject item = response["result"]["content"].add<JsonObject>();
item["type"] = "text";
item["text"] = message;
}
bool validScreenState(const char* state) {
DEBUG_MSG("Validando estado: ");
DEBUG_MSG(state);
DEBUG_BR();
if (state == nullptr) return false;
for (size_t i = 0; i < VALID_STATES_COUNT; ++i) {
if (strcmp(state, VALID_STATES[i]) == 0) return true;
}
return false;
}
// ---------- Métodos MCP ----------
void handleInitialize(JsonDocument& response, JsonVariantConst id) {
response["jsonrpc"] = "2.0";
setResponseId(response, id);
response["result"]["protocolVersion"] = MCP_PROTOCOL_VERSION;
response["result"]["capabilities"]["tools"]["listChanged"] = false;
response["result"]["serverInfo"]["name"] = "arduino-uno-r4-wifi";
response["result"]["serverInfo"]["version"] = "1.0.0";
}
void handleToolsList(JsonDocument& response, JsonVariantConst id) {
response["jsonrpc"] = "2.0";
setResponseId(response, id);
JsonArray tools = response["result"]["tools"].to<JsonArray>();
JsonObject screenTool = tools.add<JsonObject>();
screenTool["name"] = "pantalla";
screenTool["description"] = "Muestra en la matriz LED uno de los estados permitidos.";
JsonObject screenSchema = screenTool["inputSchema"].to<JsonObject>();
screenSchema["type"] = "object";
JsonObject screenProperties = screenSchema["properties"].to<JsonObject>();
JsonObject stateProperty = screenProperties["estado"].to<JsonObject>();
stateProperty["type"] = "string";
JsonArray states = stateProperty["enum"].to<JsonArray>();
for (size_t i = 0; i < VALID_STATES_COUNT; ++i) {
states.add(VALID_STATES[i]);
}
screenSchema["required"].add("estado");
screenSchema["additionalProperties"] = false;
JsonObject fanTool = tools.add<JsonObject>();
fanTool["name"] = "ventilador";
fanTool["description"] = "Fija la velocidad del ventilador entre 0 y 100.";
JsonObject fanSchema = fanTool["inputSchema"].to<JsonObject>();
fanSchema["type"] = "object";
JsonObject fanProperties = fanSchema["properties"].to<JsonObject>();
JsonObject speedProperty = fanProperties["velocidad"].to<JsonObject>();
speedProperty["type"] = "integer";
speedProperty["minimum"] = 0;
speedProperty["maximum"] = 100;
fanSchema["required"].add("velocidad");
fanSchema["additionalProperties"] = false;
}
void handleToolCall(JsonDocument& request, JsonDocument& response,
JsonVariantConst id) {
const char* name = request["params"]["name"].as<const char*>();
JsonObjectConst arguments = request["params"]["arguments"].as<JsonObjectConst>();
#if DEBUG
DEBUG_MSG("JSON recibido:");
serializeJson(request, Serial);
DEBUG_BR();
#endif
DEBUG_MSG("Name: ");
DEBUG_MSG(name);
DEBUG_BR();
if (strcmp(name, "pantalla") == 0) {
const char* state = arguments["estado"].as<const char*>();
if (!validScreenState(state)) {
makeToolError(response, id,
"estado no válido; consulte tools/list para ver los permitidos");
return;
}
pantalla(state);
makeToolSuccess(response, id, "Estado de pantalla recibido");
return;
}
if (strcmp(name, "ventilador") == 0) {
JsonVariantConst speedValue = arguments["velocidad"];
if (!speedValue.is<int>()) {
makeToolError(response, id, "velocidad debe ser un número entero");
return;
}
const int speed = speedValue.as<int>();
if (speed < 0 || speed > 100) {
makeToolError(response, id, "velocidad debe estar entre 0 y 100");
return;
}
ventilador(static_cast<uint8_t>(speed));
makeToolSuccess(response, id, "Velocidad del ventilador recibida");
return;
}
makeToolError(response, id, "Herramienta desconocida");
}
void processMcpRequest(WiFiClient& client, const String& body) {
JsonDocument request;
JsonDocument response;
DeserializationError error = deserializeJson(request, body);
if (error) {
makeError(response, JsonVariantConst(), -32700, "Parse error");
sendJsonResponse(client, 200, "OK", response);
return;
}
const char* method = request["method"] | "";
JsonVariantConst id = request["id"];
const bool isNotification = id.isNull();
if (strcmp(method, "notifications/initialized") == 0) {
sendEmptyResponse(client, 202, "Accepted");
return;
}
if (isNotification) {
sendEmptyResponse(client, 202, "Accepted");
return;
}
if (strcmp(method, "initialize") == 0) {
handleInitialize(response, id);
} else if (strcmp(method, "ping") == 0) {
response["jsonrpc"] = "2.0";
setResponseId(response, id);
response["result"].to<JsonObject>();
} else if (strcmp(method, "tools/list") == 0) {
handleToolsList(response, id);
} else if (strcmp(method, "tools/call") == 0) {
handleToolCall(request, response, id);
} else {
makeError(response, id, -32601, "Method not found");
}
sendJsonResponse(client, 200, "OK", response);
}
// ---------- Servidor ----------
bool readHttpRequest(WiFiClient& client, String& method, String& path,
String& body) {
client.setTimeout(CLIENT_TIMEOUT_MS);
String requestLine = client.readStringUntil('\n');
requestLine.trim();
const int firstSpace = requestLine.indexOf(' ');
const int secondSpace = requestLine.indexOf(' ', firstSpace + 1);
if (firstSpace <= 0 || secondSpace <= firstSpace) return false;
method = requestLine.substring(0, firstSpace);
path = requestLine.substring(firstSpace + 1, secondSpace);
size_t contentLength = 0;
while (true) {
String header = client.readStringUntil('\n');
header.trim();
if (header.length() == 0) break;
const int separator = header.indexOf(':');
if (separator > 0) {
String name = header.substring(0, separator);
name.toLowerCase();
if (name == "content-length") {
contentLength = static_cast<size_t>(header.substring(separator + 1).toInt());
}
}
}
if (contentLength > MAX_HTTP_BODY) {
sendEmptyResponse(client, 413, "Payload Too Large");
return false;
}
body = "";
body.reserve(contentLength + 1);
const unsigned long deadline = millis() + CLIENT_TIMEOUT_MS;
while (body.length() < contentLength &&
static_cast<long>(deadline - millis()) > 0) {
while (client.available() && body.length() < contentLength) {
body += static_cast<char>(client.read());
}
}
return body.length() == contentLength;
}
void handleClient(WiFiClient& client) {
String method;
String path;
String body;
if (!readHttpRequest(client, method, path, body)) return;
if (path != MCP_PATH) {
sendEmptyResponse(client, 404, "Not Found");
} else if (method == "OPTIONS") {
// Respuesta al preflight CORS del navegador.
sendEmptyResponse(client, 204, "No Content");
} else if (method == "POST") {
processMcpRequest(client, body);
} else if (method == "GET") {
// Este servidor no abre un canal SSE iniciado por GET.
sendEmptyResponse(client, 405, "Method Not Allowed");
} else {
sendEmptyResponse(client, 405, "Method Not Allowed");
}
}
void connectWiFi() {
if (WiFi.status() == WL_NO_MODULE) {
DEBUG_MSG("No se ha detectado el módulo WiFi.");
while (true) delay(1000);
}
WiFi.config(MCP_IP, MCP_DNS, MCP_GATEWAY, MCP_SUBNET);
DEBUG_MSG("Conectando a ");
DEBUG_MSG(WIFI_SSID);
while (WiFi.begin(WIFI_SSID, WIFI_PASSWORD) != WL_CONNECTED) {
DEBUG_MSG('.');
delay(3000);
}
DEBUG_BR();
DEBUG_MSG("Servidor MCP: http://");
DEBUG_MSG(WiFi.localIP());
DEBUG_MSG(':');
DEBUG_MSG(MCP_PORT);
DEBUG_MSG(MCP_PATH);
DEBUG_BR();
}
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000) {}
// La matriz 12x8 queda lista para que pantalla() cargue imágenes.
matrix.begin();
matrix.loadFrame(EMPTY_FRAME);
connectWiFi();
mcpServer.begin();
}
void loop() {
WiFiClient client = mcpServer.available();
if (client) {
handleClient(client);
client.stop();
}
}