mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-10-25 08:10:15 +00:00
Rework MCP server
This commit is contained in:
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";
|
||||
|
||||
Reference in new issue
Block a user