Rework MCP server

This commit is contained in:
crschnick committed 2026-04-06 08:35:54 +00:00
1 parent d0e9529b6f
commit 9983ecb4fa
9 files changed
+70 -99

No files matched your search

@@ -46,6 +46,11 @@ public class AppMcpServer {
(serverRequest) -> McpTransportContext.EMPTY,
null);
var prompt = McpSchemaFiles.load("prompt.md");
var effectivePrompt = AppPrefs.get().mcpAdditionalContext().getValue() != null ?
prompt.replace("__CUSTOM__", AppPrefs.get().mcpAdditionalContext().getValue()) :
prompt.replace("__CUSTOM__", "");
McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)
.serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())
.capabilities(McpSchema.ServerCapabilities.builder()
@@ -53,7 +58,7 @@ public class AppMcpServer {
.tools(true)
.prompts(false)
.build())
.instructions(AppPrefs.get().mcpAdditionalContext().getValue())
.instructions(effectivePrompt)
.build();
var readOnlyTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
@@ -66,7 +71,6 @@ public class AppMcpServer {
var mutationTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
mutationTools.add(McpTools.openTerminal());
mutationTools.add(McpTools.openTerminalInline());
mutationTools.add(McpTools.createFile());
mutationTools.add(McpTools.writeFile());
mutationTools.add(McpTools.createDirectory());
@@ -2,6 +2,7 @@ package io.xpipe.app.beacon.mcp;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
@@ -113,13 +114,13 @@ public interface McpToolHandler
return b;
}
public FilePath getFilePath(String key) throws BeaconClientException {
public FilePath getFilePath(ShellControl sc, String key) throws Exception {
var s = getStringArgument(key);
var path = FilePath.parse(s);
if (path == null) {
throw new BeaconClientException("Invalid argument for key " + key);
}
return path;
return path.resolveTildeHome(sc.view().userHome());
}
public DataStoreEntryRef<?> getDataStoreRef(String name) throws BeaconClientException {
@@ -130,7 +131,7 @@ public interface McpToolHandler
if (found.size() > 1) {
throw new BeaconClientException("Multiple connections found: "
+ found.stream().map(DataStoreEntry::getName).toList());
+ found.stream().map(entry -> DataStorage.get().getStorePath(entry).toString()).toList());
}
var e = found.getFirst();
@@ -63,6 +63,9 @@ public final class McpTools {
+ syncToolSpecification.tool().description())
.collect(Collectors.joining("\n"));
var muEnabled = AppPrefs.get().enableMcpMutationTools().get();
var muStatus = muEnabled ? "Right now, the mutation tools are enabled." : "Right now, the mutation tools are disabled. When you enable them in the settings menu, the MCP client might need a reconnect to see the changes.";
var text = """
The XPipe MCP server offers the following read-only tools:
%s
@@ -71,7 +74,8 @@ public final class McpTools {
You can also enable the following potentially destructive tools in the settings menu:
%s
These tools can perform write operations and other actions that might be potentially destructive.
""".formatted(roList, muList);
%s
""".formatted(roList, muList, muStatus);
return McpSchema.CallToolResult.builder()
.addTextContent(text)
@@ -138,10 +142,8 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var filter = req.getOptionalStringArgument("filter");
var entries = filter.isPresent()
? DataStorageQuery.queryUserInput(filter.get())
: DataStorage.get().getStoreEntries();
var filter = req.getStringArgument("filter");
var entries = DataStorageQuery.queryUserInput(filter);
var list = new ArrayList<ConnectionResource>();
for (var e : entries) {
@@ -192,10 +194,10 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var shellStore = req.getShellStoreRef(system, false);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
if (!fs.fileExists(path)) {
@@ -218,12 +220,12 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var recursive = req.getOptionalBooleanArgument("recursive").orElse(false);
var shellStore = req.getShellStoreRef(system, false);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var fs = new ConnectionFileSystem(shellSession.getControl());
var path = req.getFilePath(shellSession.getControl(), "path");
if (!fs.directoryExists(path)) {
throw new BeaconClientException("Directory " + path + " does not exist");
@@ -246,12 +248,11 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var recursive = req.getOptionalBooleanArgument("recursive").orElse(false);
var pattern = req.getStringArgument("name");
var shellStore = req.getShellStoreRef(system, false);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
if (!fs.directoryExists(path)) {
@@ -259,7 +260,7 @@ public final class McpTools {
}
var regex = Pattern.compile(DataStorageQuery.toRegex(pattern));
try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) {
try (var stream = fs.listFiles(fs, path)) {
var list = stream.toList();
var builder = McpSchema.CallToolResult.builder();
list.stream()
@@ -280,10 +281,10 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var shellStore = req.getShellStoreRef(system, false);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
if (!fs.fileExists(path) && !fs.directoryExists(path)) {
@@ -321,10 +322,10 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
if (fs.fileExists(path)) {
@@ -353,11 +354,11 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var content = req.getStringArgument("content");
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
var b = content.getBytes(StandardCharsets.UTF_8);
@@ -377,10 +378,10 @@ public final class McpTools {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var path = req.getFilePath("path");
var system = req.getStringArgument("system");
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var path = req.getFilePath(shellSession.getControl(), "path");
var fs = new ConnectionFileSystem(shellSession.getControl());
if (fs.fileExists(path)) {
@@ -406,12 +407,8 @@ public final class McpTools {
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var out = ProcessControlProvider.get().executeMcpCommand(shellSession.getControl(), command);
var formatted = CommandDialog.formatOutput(out);
return McpSchema.CallToolResult.builder()
.addTextContent(formatted)
.build();
var r = ProcessControlProvider.get().executeMcpCommand(shellSession.getControl(), command);
return r;
}))
.build();
}
@@ -423,11 +420,11 @@ public final class McpTools {
.callHandler(McpToolHandler.of((req) -> {
var system = req.getStringArgument("system");
var script = req.getDataStoreRef("script");
var directory = req.getFilePath("directory");
var arguments = req.getStringArgument("arguments");
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var directory = req.getFilePath(shellSession.getControl(), "directory");
var clazz = Class.forName(
AppExtensionManager.getInstance()
@@ -479,34 +476,6 @@ public final class McpTools {
.build();
}
public static McpServerFeatures.SyncToolSpecification openTerminalInline() throws IOException {
var tool = McpSchemaFiles.loadTool("open_terminal_inline.json");
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler(McpToolHandler.of((req) -> {
var system = req.getStringArgument("system");
var directory = req.getOptionalStringArgument("directory");
var shellStore = req.getShellStoreRef(system, true);
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
var script = shellSession
.getControl()
.prepareTerminalOpen(
TerminalInitScriptConfig.ofName(
shellStore.get().getName()),
directory.isPresent()
? WorkingDirectoryFunction.fixed(FilePath.parse(directory.get()))
: WorkingDirectoryFunction.none());
var json = JsonNodeFactory.instance.objectNode();
json.put("command", script);
return McpSchema.CallToolResult.builder()
.structuredContent(JacksonMapper.getDefault().writeValueAsString(json))
.build();
}))
.build();
}
public static McpServerFeatures.SyncToolSpecification toggleState() throws IOException {
var tool = McpSchemaFiles.loadTool("toggle_state.json");
return McpServerFeatures.SyncToolSpecification.builder()
@@ -1,5 +1,6 @@
package io.xpipe.app.ext;
import io.modelcontextprotocol.spec.McpSchema;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserStoreSessionTab;
import io.xpipe.app.comp.base.ModalOverlay;
@@ -58,7 +59,7 @@ public abstract class ProcessControlProvider {
public abstract ShellDialect getEffectiveLocalDialect();
public abstract String executeMcpCommand(ShellControl sc, String command) throws Exception;
public abstract McpSchema.CallToolResult executeMcpCommand(ShellControl sc, String command) throws Exception;
public ShellDialect getNextFallbackDialect() {
var av = getAvailableLocalDialects();
@@ -20,7 +20,7 @@ public enum SecretQueryState {
yield "Authentication operation was cancelled";
}
case NON_INTERACTIVE -> {
yield "Session is not interactive but required user input for authentication";
yield "Session is not interactive but required user input for authentication as no password is preconfigured";
}
case FIXED_SECRET_WRONG -> {
yield "Authentication failed: Provided authentication secret was not accepted by the server, probably because it is incorrect";