Commit 9c971dfc authored by Andrey Filippov's avatar Andrey Filippov

Started MCP

parent f3541d48
package com.elphel.imagej.mcp;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.elphel.imagej.correction.Eyesis_Correction;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class McpServer {
private static McpServer INSTANCE;
private final Eyesis_Correction owner;
private final int port;
private HttpServer server;
public static synchronized McpServer startIfNeeded(Eyesis_Correction owner, int port) {
if (INSTANCE != null) {
return INSTANCE;
}
McpServer instance = new McpServer(owner, port);
instance.start();
INSTANCE = instance;
return instance;
}
private McpServer(Eyesis_Correction owner, int port) {
this.owner = owner;
this.port = port;
}
private void start() {
try {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
} catch (IOException e) {
System.out.println("MCP: failed to start HTTP server on port " + port + ": " + e.getMessage());
return;
}
server.createContext("/mcp/status", new StatusHandler());
server.createContext("/mcp/dialog", new DialogHandler());
server.createContext("/mcp/dialog/values", new DialogValuesHandler());
server.createContext("/mcp/button", new ButtonHandler());
server.setExecutor(null);
server.start();
System.out.println("MCP: server started on http://127.0.0.1:" + port);
}
private class StatusHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = buildStatusJson();
sendJson(exchange, 200, response);
}
}
private class DialogHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = buildDialogJson();
sendJson(exchange, 200, response);
}
}
private class DialogValuesHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendJson(exchange, 405, "{\"ok\":false,\"error\":\"POST required\"}");
return;
}
Map<String, String> params = parseParams(exchange);
String label = params.get("label");
String value = params.get("value");
if (label == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing label\"}");
return;
}
McpDialogRegistry.setValue(label, value);
sendJson(exchange, 200, "{\"ok\":true}");
}
}
private class ButtonHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendJson(exchange, 405, "{\"ok\":false,\"error\":\"POST required\"}");
return;
}
Map<String, String> params = parseParams(exchange);
String label = params.get("label");
if (label == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing label\"}");
return;
}
owner.triggerCommand(label);
sendJson(exchange, 200, "{\"ok\":true}");
}
}
private String buildStatusJson() {
int stopRequested = owner.getSyncStopRequested();
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"running\":").append(owner.isSyncRunning());
sb.append(",\"stopRequested\":").append(stopRequested);
sb.append(",\"buttonLabel\":\"").append(jsonEscape(owner.getSyncButtonLabel())).append("\"");
sb.append("}");
return sb.toString();
}
private String buildDialogJson() {
McpDialogSession session = McpDialogRegistry.getCurrent();
if (session == null) {
return "{\"ok\":true,\"dialog\":null}";
}
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"dialog\":{");
sb.append("\"id\":\"").append(jsonEscape(session.getId())).append("\",");
sb.append("\"title\":\"").append(jsonEscape(session.getTitle())).append("\",");
sb.append("\"fields\":[");
List<McpDialogField> fields = session.getFields();
for (int i = 0; i < fields.size(); i++) {
McpDialogField f = fields.get(i);
if (i > 0) {
sb.append(",");
}
sb.append("{");
sb.append("\"type\":\"").append(f.type.name().toLowerCase()).append("\"");
if (f.label != null) {
sb.append(",\"label\":\"").append(jsonEscape(f.label)).append("\"");
}
if (f.tab != null) {
sb.append(",\"tab\":\"").append(jsonEscape(f.tab)).append("\"");
}
if (f.tooltip != null) {
sb.append(",\"tooltip\":\"").append(jsonEscape(f.tooltip)).append("\"");
}
if (f.units != null) {
sb.append(",\"units\":\"").append(jsonEscape(f.units)).append("\"");
}
if (f.defaultValue != null) {
sb.append(",\"default\":\"").append(jsonEscape(f.defaultValue)).append("\"");
}
if (f.choices != null) {
sb.append(",\"choices\":[");
for (int c = 0; c < f.choices.length; c++) {
if (c > 0) {
sb.append(",");
}
sb.append("\"").append(jsonEscape(f.choices[c])).append("\"");
}
sb.append("]");
}
sb.append("}");
}
sb.append("]}}" );
return sb.toString();
}
private static void sendJson(HttpExchange exchange, int code, String body) throws IOException {
byte[] data = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(code, data.length);
OutputStream os = exchange.getResponseBody();
os.write(data);
os.close();
}
private static Map<String, String> parseParams(HttpExchange exchange) throws IOException {
Map<String, String> params = new HashMap<String, String>();
URI uri = exchange.getRequestURI();
if (uri != null && uri.getQuery() != null) {
parseQueryString(uri.getQuery(), params);
}
if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
byte[] body = exchange.getRequestBody().readAllBytes();
if (body.length > 0) {
String raw = new String(body, StandardCharsets.UTF_8);
parseQueryString(raw, params);
}
}
return params;
}
private static void parseQueryString(String raw, Map<String, String> out) {
if (raw == null || raw.isEmpty()) {
return;
}
String[] pairs = raw.split("&");
for (String pair : pairs) {
if (pair.isEmpty()) {
continue;
}
int idx = pair.indexOf('=');
String key = idx >= 0 ? pair.substring(0, idx) : pair;
String value = idx >= 0 ? pair.substring(idx + 1) : "";
out.put(urlDecode(key), urlDecode(value));
}
}
private static String urlDecode(String value) {
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
} catch (Exception e) {
return value;
}
}
private static String jsonEscape(String value) {
if (value == null) {
return "";
}
StringBuilder sb = new StringBuilder(value.length() + 10);
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '\\': sb.append("\\\\"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default: sb.append(c); break;
}
}
return sb.toString();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment