diff --git a/app/src/main/java/io/xpipe/app/mcp/HttpStreamableServerTransportProvider.java b/app/src/main/java/io/xpipe/app/mcp/HttpStreamableServerTransportProvider.java index 10d00b4ad..135af4343 100644 --- a/app/src/main/java/io/xpipe/app/mcp/HttpStreamableServerTransportProvider.java +++ b/app/src/main/java/io/xpipe/app/mcp/HttpStreamableServerTransportProvider.java @@ -13,13 +13,13 @@ import io.modelcontextprotocol.server.McpTransportContextExtractor; import io.modelcontextprotocol.spec.*; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.KeepAliveScheduler; +import io.xpipe.app.issue.TrackEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.*; -import java.net.http.HttpRequest; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; @@ -74,7 +74,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe */ private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); - private McpTransportContextExtractor contextExtractor; + private final McpTransportContextExtractor contextExtractor; /** * Flag indicating if the transport is shutting down. @@ -261,13 +261,15 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe } } - private void sendError(HttpExchange exchange, int code, String message) throws IOException { + public void sendError(HttpExchange exchange, int code, String message) throws IOException { var b = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0]; exchange.getResponseHeaders().add("Content-Encoding", UTF_8); exchange.sendResponseHeaders(code, b.length != 0 ? b.length : -1); try (OutputStream os = exchange.getResponseBody()) { os.write(b); } + + TrackEvent.error("MCP server error: " + message); } public void doPost(HttpExchange exchange) diff --git a/app/src/main/java/io/xpipe/app/mcp/McpServer.java b/app/src/main/java/io/xpipe/app/mcp/McpServer.java index 41c3a7e4d..d51e93694 100644 --- a/app/src/main/java/io/xpipe/app/mcp/McpServer.java +++ b/app/src/main/java/io/xpipe/app/mcp/McpServer.java @@ -5,11 +5,16 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; +import io.xpipe.app.core.AppOpenArguments; import io.xpipe.app.core.AppProperties; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.StorageListener; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.beacon.BeaconServerException; import lombok.SneakyThrows; import lombok.Value; @@ -90,13 +95,64 @@ public class McpServer { @Override public void handle(HttpExchange exchange) throws IOException { try (exchange) { + if (AppPrefs.get() == null) { + transportProvider.sendError(exchange, 503, "Not initialized"); + return; + } + + if (!AppPrefs.get().enableMcpServer().get()) { + transportProvider.sendError(exchange, 403, "MCP server is not enabled in the API settings menu"); + if (exchange.getRequestMethod().equals("POST")) { + ThreadHelper.runAsync(() -> { + ErrorEventFactory.fromMessage( + "An external request was made to the XPipe MCP server, however the MCP server is not enabled in the API settings menu") + .expected() + .handle(); + }); + } + return; + } + + if (!AppPrefs.get().disableApiAuthentication().get()) { + var apiKey = exchange.getRequestHeaders().getFirst("Authorization"); + if (apiKey == null) { + transportProvider.sendError(exchange, 403, "Header Authorization is not set"); + if (exchange.getRequestMethod().equals("POST")) { + ThreadHelper.runAsync(() -> { + ErrorEventFactory.fromMessage( + "An external request was made to the XPipe MCP server without the header Authorization set. Please configure your MCP client with the Bearer API token you can find the API settings menu") + .expected() + .handle(); + }); + } + return; + } + + var correct = apiKey.replace("Bearer ", "").equals(AppPrefs.get().apiKey().get()); + if (!correct) { + transportProvider.sendError(exchange, 403, "Invalid API key"); + if (exchange.getRequestMethod().equals("POST")) { + ThreadHelper.runAsync(() -> { + ErrorEventFactory.fromMessage("The Authorization header sent by the MCP client is not correct") + .expected() + .handle(); + }); + } + return; + } + } + if (exchange.getRequestMethod().equals("GET")) { transportProvider.doGet(exchange); } else if (exchange.getRequestMethod().equals("POST")) { transportProvider.doPost(exchange); + } else if (exchange.getRequestMethod().equals("DELETE")) { + transportProvider.doDelete(exchange); } else { transportProvider.doOther(exchange); } + } finally { + exchange.close(); } } }; diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index fe1652f3b..bb8eb4093 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -60,6 +60,8 @@ public class AppPrefs { .valueClass(Boolean.class) .requiresRestart(true) .build()); + final BooleanProperty enableMcpServer = + mapVaultShared(new SimpleBooleanProperty(false), "enableMcpServer", Boolean.class, false); final BooleanProperty enableHttpApi = mapVaultShared(new SimpleBooleanProperty(false), "enableHttpApi", Boolean.class, false); final BooleanProperty dontAutomaticallyStartVmSshServer = @@ -305,6 +307,10 @@ public class AppPrefs { return enableHttpApi; } + public ObservableBooleanValue enableMcpServer() { + return enableMcpServer; + } + public ObservableBooleanValue pinLocalMachineOnStartup() { return pinLocalMachineOnStartup; } diff --git a/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java b/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java index eb174b636..a4895d7e0 100644 --- a/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java @@ -32,10 +32,13 @@ public class HttpApiCategory extends AppPrefsCategory { .addComp(new ButtonComp(AppI18n.observable("openApiDocsButton"), () -> { DocumentationLink.API.open(); })) + .pref(prefs.enableMcpServer) + .addToggle(prefs.enableMcpServer) .pref(prefs.apiKey) .addComp(new TextFieldComp(prefs.apiKey).maxWidth(getCompWidth()), prefs.apiKey) .pref(prefs.disableApiAuthentication) - .addToggle(prefs.disableApiAuthentication)) + .addToggle(prefs.disableApiAuthentication) + ) .buildComp(); } }