Commit 094e24bc authored by Andrey Filippov's avatar Andrey Filippov

Debugging basic MCP functionality, added simple MCP file management

parent 78cc5e61
......@@ -3407,8 +3407,13 @@ public class CLTParameters {
}
public boolean showJDialog() {
return showJDialog(false);
}
// codex 2026-01-27: optional modeless dialog for MCP/GUI co-use
public boolean showJDialog(boolean nonBlocking) {
// GenericDialog gd = new GenericDialog("Set CLT parameters");
GenericJTabbedDialogMcp gd = new GenericJTabbedDialogMcp("Set CLT parameters",1090,900); // codex 2026-01-25
GenericJTabbedDialogMcp gd = new GenericJTabbedDialogMcp("Set CLT parameters",1090,900, !nonBlocking); // codex 2026-01-25
gd.addTab ("General", "General parameters");
gd.addNumericField("Nominal (rectilinear) disparity between side of square cameras (pix)", this.disparity, 3,7,"pix",
"Used when rendering 4 images");
......@@ -4926,6 +4931,17 @@ public class CLTParameters {
// gd.buildDialog();
if (nonBlocking && !gd.mcp_mode) {
gd.setOnOk(new Runnable() {
@Override
public void run() {
gd.resetReadPosition();
applyCltDialog(gd);
}
});
gd.showDialogNonBlocking();
return true;
}
gd.showDialog();
// System.out.println("gd.wasCanceled="+gd.wasCanceled());
/*
......@@ -4933,6 +4949,12 @@ public class CLTParameters {
WindowTools.addScrollBars(gd);
gd.showDialog();
*/
return applyCltDialog(gd);
}
// codex 2026-01-27: apply dialog values after OK
private boolean applyCltDialog(GenericJTabbedDialogMcp gd) {
if (gd.wasCanceled()) return false;
this.disparity= gd.getNextNumber();
this.transform_size= (int) gd.getNextNumber();
......@@ -5964,7 +5986,6 @@ public class CLTParameters {
return true;
}
public boolean showTsDialog() {
GenericDialog gd = new GenericDialog("Set CLT tiles to surfaces assignment parameters");
gd.addCheckbox ("Do not assign tiles to the surface edges (not having all 8 neighbors)", this.tsNoEdge);
......@@ -6120,8 +6141,10 @@ public class CLTParameters {
this.taEnWeakFgnd= gd.getNextBoolean();
this.taEnFlaps= gd.getNextBoolean();
this.taEnMismatch= gd.getNextBoolean();
return true;
}
public boolean modifyZCorr (String title) {
if (z_corr_map == null){
z_corr_map = new HashMap<String,Double>();
......
......@@ -1197,7 +1197,14 @@ public class EyesisCorrectionParameters {
public boolean showCLTBatchDialog(String title,
CLTParameters clt_parameters) {
GenericJTabbedDialogMcp gd = new GenericJTabbedDialogMcp(title,1000,1000); // codex 2026-01-25
return showCLTBatchDialog(title, clt_parameters, false);
}
// codex 2026-01-27: optional modeless dialog for MCP/GUI co-use
public boolean showCLTBatchDialog(String title,
CLTParameters clt_parameters,
boolean nonBlocking) {
GenericJTabbedDialogMcp gd = new GenericJTabbedDialogMcp(title,1000,1000, !nonBlocking); // codex 2026-01-25
updateAuxFromMain();
......@@ -1402,7 +1409,25 @@ public class EyesisCorrectionParameters {
}
// WindowTools.addScrollBars(gd);
if (nonBlocking && !gd.mcp_mode) {
gd.setOnOk(new Runnable() {
@Override
public void run() {
gd.resetReadPosition();
applyCltBatchDialog(gd, clt_parameters);
}
});
gd.showDialogNonBlocking();
return true;
}
gd.showDialog();
return applyCltBatchDialog(gd, clt_parameters);
}
// codex 2026-01-27: apply dialog values after OK
private boolean applyCltBatchDialog(GenericJTabbedDialogMcp gd,
CLTParameters clt_parameters) {
if (gd.wasCanceled()) return false;
this.tile_processor_gpu = gd.getNextString(); if (gd.getNextBoolean()) selectGPUSourceDirectory(false, false);
......@@ -1767,7 +1792,7 @@ public class EyesisCorrectionParameters {
base_path=base_path.resolve(Paths.get(dir_map.get("rootDirectory")));
File base_dir = new File(base_path.toString());
if (!base_dir.exists()) {
base_dir.mkdirs();
base_dir.mkdirs(); // codex 2026-01-28: mkdirs side-effect (consider guarding)
}
}
// set sourceDirectory:
......@@ -1783,7 +1808,7 @@ public class EyesisCorrectionParameters {
Path source_path = Paths.get(this.sourceDirectory);
File source_dir = new File(source_path.toString());
if (!source_dir.exists()) {
source_dir.mkdirs();
source_dir.mkdirs(); // codex 2026-01-28: mkdirs side-effect (consider guarding)
}
useCuasSeedDir = false;
......@@ -1796,7 +1821,7 @@ public class EyesisCorrectionParameters {
File dir_file = new File(dir_path.toString());
if ((i != KEY_INDEX_UAS_LOGS) && (i != KEY_INDEX_SKY_MASK)) { // cuasUasLogs, cuasSkyMask are files, not directories
if (!dir_file.exists()) {
dir_file.mkdirs();
dir_file.mkdirs(); // codex 2026-01-28: mkdirs side-effect (consider guarding)
}
}
dir_string = dir_path.toString();
......
......@@ -48,12 +48,20 @@ public class GenericJTabbedDialog implements ActionListener {
private int read_tab=0,read_component=0; // current number of tab/component to be read next
String result=null;
private JDialog jd;
// codex 2026-01-27: optional callbacks for modeless dialogs
private Runnable onOk;
private Runnable onCancel;
public GenericJTabbedDialog(String title) {
this(title, 600, 800);
}
public GenericJTabbedDialog(String title, int width, int height) {
this(title, width, height, true);
}
// codex 2026-01-27: allow modeless dialog for non-blocking UI
public GenericJTabbedDialog(String title, int width, int height, boolean modal) {
// final JFrame frame= new JFrame();
jd = new JDialog(frame , title, true);
jd = new JDialog(frame , title, modal);
components.add(new ArrayList<JComponent>());
labels.add(new ArrayList<JLabel>());
tabs.add (new JPanel(false)); // first (yet nameless tab)
......@@ -341,14 +349,48 @@ public class GenericJTabbedDialog implements ActionListener {
return false;
}
// codex 2026-01-27: modeless dialog
public void showDialogNonBlocking() {
buildDialog();
jd.setVisible(true);
}
// codex 2026-01-27: reset read cursor before applying values
public void resetReadPosition() {
read_tab = 0;
read_component = 0;
}
// codex 2026-01-27: callbacks for modeless dialogs
public void setOnOk(Runnable onOk) {
this.onOk = onOk;
}
public void setOnCancel(Runnable onCancel) {
this.onCancel = onCancel;
}
// codex 2026-01-27: allow external close (for MCP/GUI coordination)
public void closeDialog() {
if (jd != null) {
jd.dispose();
}
}
@Override
public void actionPerformed(ActionEvent e) {
// System.out.println(e.getActionCommand());
if (e.getActionCommand().equals("Cancel")) {
result = e.getActionCommand();
if (onCancel != null) {
onCancel.run();
}
jd.dispose();
} else if (e.getActionCommand().equals("OK")) {
result = e.getActionCommand();
if (onOk != null) {
onOk.run();
}
jd.dispose();
// Added 07/18/2021 - button names starting with "_" exit from dialog
} else if ((e.getActionCommand().length()>0) && (e.getActionCommand().charAt(0) == '_')){
......
......@@ -27,11 +27,21 @@ public class GenericJTabbedDialogMcp extends GenericJTabbedDialog{
this.dialogTitle = title;
this.mcp_mode = defaultMcpMode;
}
// codex 2026-01-27: allow modeless dialog creation
public GenericJTabbedDialogMcp(String title, int width, int height, boolean modal) {
super(title, width, height, modal);
this.dialogTitle = title;
this.mcp_mode = defaultMcpMode;
}
// codex 2026-01-25: global MCP mode default
public static void setDefaultMcpMode(boolean enabled) {
defaultMcpMode = enabled;
}
// codex 2026-01-27: expose MCP mode default
public static boolean isDefaultMcpMode() {
return defaultMcpMode;
}
private void addMcpField(McpDialogField field) {
mcpFields.add(field);
......@@ -248,6 +258,14 @@ public class GenericJTabbedDialogMcp extends GenericJTabbedDialog{
else return super.showDialog();
}
// codex 2026-01-27: modeless dialog for GUI mode
public void showDialogNonBlocking() {
if (mcp_mode) {
buildDialog();
}
else super.showDialogNonBlocking();
}
@Override
public String showDialogAny() {
if (mcp_mode) {
......
......@@ -67,6 +67,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
......@@ -120,6 +121,7 @@ import com.elphel.imagej.gpu.JCuda_ImageJ_Example_Plugin;
import com.elphel.imagej.ims.DjiSrt;
import com.elphel.imagej.ims.DjiSrtReader;
import com.elphel.imagej.mcp.McpServer;
import com.elphel.imagej.mcp.FileSelectMcp;
import com.elphel.imagej.ims.EventLogger;
import com.elphel.imagej.ims.QuatVertLMA;
import com.elphel.imagej.ims.UasLogReader;
......@@ -475,6 +477,8 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
public static ImageStack convolutionKernelStack2 = null; // select to use for image convolution
public static ImagePlus imp_gaussian = null;
public static String DEFAULT_DIRECTORY = null;
// codex 2026-01-28: remember last MCP-selected paths per dialog title
private static final HashMap<String, String> MCP_LAST_PATHS = new HashMap<String, String>();
public static boolean ADVANCED_MODE = false; // true; // show all buttons
public static boolean DCT_MODE = false; // true; // show all buttons
public static boolean MODE_3D = false; // 3D-related commands
......@@ -531,6 +535,8 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
LogTee.install(); // once, early
// codex 2026-01-25: start MCP server + enable MCP dialog mode by default
GenericJTabbedDialogMcp.setDefaultMcpMode(getMcpMode());
// codex 2026-01-27: enable MCP-mode SyncCommand control
SYNC_COMMAND.setMcpMode(getMcpMode());
McpServer.startIfNeeded(this, getMcpPort());
// codex 2026-01-25: optional SLF4J/logback root level override
applySlf4jRootLevel();
......@@ -1565,6 +1571,23 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
return this.SYNC_COMMAND.buttonLabel;
}
// codex 2026-01-27: MCP interrupt controls
public boolean requestSyncStop(boolean confirm, boolean asap) {
return this.SYNC_COMMAND.requestStop(confirm, asap);
}
public boolean confirmSyncStop(boolean stop) {
return this.SYNC_COMMAND.confirmStop(stop);
}
public boolean isSyncConfirmPending() {
return this.SYNC_COMMAND.isConfirmPending();
}
public boolean isSyncConfirmConvenient() {
return this.SYNC_COMMAND.isConfirmConvenient();
}
private int getMcpPort() {
String value = System.getProperty("elphel.mcp.port");
if (value == null || value.isEmpty()) {
......@@ -4739,16 +4762,16 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
/* ======================================================================== */
} else if (label.equals("Setup CLT Batch parameters")) {
DEBUG_LEVEL = MASTER_DEBUG_LEVEL;
CORRECTION_PARAMETERS.showCLTBatchDialog("CLT Batch parameters", CLT_PARAMETERS);
CORRECTION_PARAMETERS.showCLTBatchDialog("CLT Batch parameters", CLT_PARAMETERS, true); // codex 2026-01-27
return;
/* ======================================================================== */
} else if (label.equals("Setup CLT parameters")) {
CLT_PARAMETERS.showJDialog();
CLT_PARAMETERS.showJDialog(true); // codex 2026-01-27
return;
/* ======================================================================== */
} else if (label.equals("Setup CLT")) {
CLT_PARAMETERS.showJDialog();
CLT_PARAMETERS.showJDialog(true); // codex 2026-01-27
return;
/* ======================================================================== */
} else if (label.equals("Infinity offset")) {
......@@ -10139,6 +10162,27 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
public static String[] selectDirectoriesOrFiles(boolean save, boolean directory, String title, String button,
FileFilter filter, String[] defaultPaths) {
if (GenericJTabbedDialogMcp.isDefaultMcpMode()) {
String defaultPath = null;
if (defaultPaths != null && defaultPaths.length > 0) {
defaultPath = defaultPaths[0];
}
String key = (title == null || title.isEmpty()) ? "default" : title;
String initial = defaultPath;
if (initial == null || initial.isEmpty()) {
initial = MCP_LAST_PATHS.get(key);
}
if (initial == null || initial.isEmpty()) {
initial = DEFAULT_DIRECTORY;
}
String[] selected = FileSelectMcp.selectMultiple(directory, title, initial);
if (selected == null || selected.length == 0) {
return null;
}
updateDefaultDirectoryFromPath(selected[0], directory);
MCP_LAST_PATHS.put(key, selected[0]);
return selected;
}
File dir = null;
String defaultPath = null;
File[] files = null;
......@@ -10211,6 +10255,9 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
public static String selectDirectoryOrFile(boolean save, boolean directory, String title, String button, FileFilter filter,
String defaultPath) {
if (GenericJTabbedDialogMcp.isDefaultMcpMode()) {
return selectDirectoryOrFileMcp(directory, title, defaultPath);
}
File dir = null;
if ((defaultPath != null) && (!defaultPath.equals(""))) {
dir = new File(defaultPath);
......@@ -10250,6 +10297,44 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
DEFAULT_DIRECTORY = fc.getCurrentDirectory().getPath();
return fc.getSelectedFile().getPath();
}
// codex 2026-01-28: MCP file selection helper (single path)
private static String selectDirectoryOrFileMcp(boolean directory, String title, String defaultPath) {
String key = (title == null || title.isEmpty()) ? "default" : title;
String initial = defaultPath;
if (initial == null || initial.isEmpty()) {
initial = MCP_LAST_PATHS.get(key);
}
if (initial == null || initial.isEmpty()) {
initial = DEFAULT_DIRECTORY;
}
String value = FileSelectMcp.selectSingle(directory, title, initial);
if (value == null) {
return null;
}
updateDefaultDirectoryFromPath(value, directory);
MCP_LAST_PATHS.put(key, value);
return value;
}
// codex 2026-01-28: update DEFAULT_DIRECTORY from a selected path
private static void updateDefaultDirectoryFromPath(String value, boolean directory) {
if (value == null || value.isEmpty()) {
return;
}
File selected = new File(value);
if (directory && !selected.isDirectory()) {
return;
}
if (selected.isDirectory()) {
DEFAULT_DIRECTORY = selected.getPath();
} else {
File parent = selected.getParentFile();
if (parent != null) {
DEFAULT_DIRECTORY = parent.getPath();
}
}
}
/*
class MultipleExtensionsFileFilter extends FileFilter {
protected String[] patterns;
......
......@@ -12,6 +12,49 @@ public class SyncCommand {
public AtomicInteger stopRequested = new AtomicInteger(0); // 0 - not requested, 1 - ASAP, 2 - gracefully
public String buttonLabel = "";
public boolean confirm = true;
// codex 2026-01-27: MCP-controlled pause/confirm
private boolean mcpMode = false;
private boolean confirmPending = false;
private Boolean confirmDecision = null;
private boolean confirmConvenient = false;
private GenericJTabbedDialog confirmDialog = null;
// codex 2026-01-27: enable MCP mode (non-GUI confirm)
public void setMcpMode(boolean enabled) {
this.mcpMode = enabled;
}
// codex 2026-01-27: request stop via MCP
public boolean requestStop(boolean confirm, boolean asap) {
this.confirm = confirm;
this.stopRequested.set(asap ? STOP_ASAP : STOP_CONVENIENT);
return true;
}
// codex 2026-01-27: confirm/cancel stop via MCP
public boolean confirmStop(boolean stop) {
synchronized (this) {
if (!confirmPending) {
return false;
}
confirmDecision = Boolean.valueOf(stop);
confirmPending = false;
if (confirmDialog != null) {
confirmDialog.closeDialog();
confirmDialog = null;
}
this.notifyAll();
}
return true;
}
public boolean isConfirmPending() {
return confirmPending;
}
public boolean isConfirmConvenient() {
return confirmConvenient;
}
public boolean interruptCommand() {
boolean asap = false;
......@@ -38,6 +81,54 @@ public class SyncCommand {
if (!confirm) {
return true;
}
if (mcpMode) {
synchronized (this) {
confirmPending = true;
confirmConvenient = convenient;
confirmDecision = null;
stopRequested.set(STOP_NO); // reset pending stop in any case.
confirmDialog = new GenericJTabbedDialog(
"Stop requested, " + (convenient ? "convenient point" : "ASAP point"), 600, 300, false);
confirmDialog.addMessage("OK to stop, Cancel to continue");
confirmDialog.setOnOk(new Runnable() {
@Override
public void run() {
synchronized (SyncCommand.this) {
confirmDecision = Boolean.TRUE;
confirmPending = false;
confirmDialog = null;
SyncCommand.this.notifyAll();
}
}
});
confirmDialog.setOnCancel(new Runnable() {
@Override
public void run() {
synchronized (SyncCommand.this) {
confirmDecision = Boolean.FALSE;
confirmPending = false;
confirmDialog = null;
SyncCommand.this.notifyAll();
}
}
});
confirmDialog.showDialogNonBlocking();
while (confirmDecision == null) {
try {
this.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
confirmPending = false;
if (confirmDialog != null) {
confirmDialog.closeDialog();
confirmDialog = null;
}
return false;
}
}
return confirmDecision.booleanValue();
}
}
GenericJTabbedDialog gd = new GenericJTabbedDialog("Stop requested, "+(convenient?"convenient point":"ASAP point"),600,300);
gd.addMessage ("OK to stop, Cancel to continue");
stopRequested.set(STOP_NO); // reset pending stop in any case.
......
......@@ -6,11 +6,14 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.elphel.imagej.correction.Eyesis_Correction;
import com.elphel.imagej.mcp.McpFsAccess;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
......@@ -49,6 +52,14 @@ public class McpServer {
server.createContext("/mcp/dialog", new DialogHandler());
server.createContext("/mcp/dialog/values", new DialogValuesHandler());
server.createContext("/mcp/button", new ButtonHandler());
server.createContext("/mcp/interrupt", new InterruptHandler());
server.createContext("/mcp/interrupt/confirm", new InterruptConfirmHandler());
server.createContext("/mcp/fs/roots", new FsRootsHandler());
server.createContext("/mcp/fs/list", new FsListHandler());
server.createContext("/mcp/fs/read", new FsReadHandler());
server.createContext("/mcp/fs/head", new FsHeadHandler());
server.createContext("/mcp/fs/tail", new FsTailHandler());
server.createContext("/mcp/fs/glob", new FsGlobHandler());
server.setExecutor(null);
server.start();
System.out.println("MCP: server started on http://127.0.0.1:" + port);
......@@ -107,12 +118,289 @@ public class McpServer {
}
}
// codex 2026-01-27: request interrupt via MCP
private class InterruptHandler 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);
boolean confirm = parseBool(params.get("confirm"), true);
boolean asap = parseBool(params.get("asap"), false);
owner.requestSyncStop(confirm, asap);
sendJson(exchange, 200, "{\"ok\":true}");
}
}
// codex 2026-01-27: confirm or cancel interrupt via MCP
private class InterruptConfirmHandler 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);
boolean stop = parseBool(params.get("stop"), true);
boolean applied = owner.confirmSyncStop(stop);
sendJson(exchange, 200, applied ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"No pending confirm\"}");
}
}
// codex 2026-01-28: list allowed roots for MCP file access
private class FsRootsHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
List<java.nio.file.Path> roots = McpFsAccess.getAllowedRoots();
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"roots\":[");
for (int i = 0; i < roots.size(); i++) {
if (i > 0) {
sb.append(",");
}
sb.append("\"").append(jsonEscape(roots.get(i).toString())).append("\"");
}
sb.append("]}");
sendJson(exchange, 200, sb.toString());
}
}
private class FsListHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
Map<String, String> params = parseParams(exchange);
String pathStr = params.get("path");
if (pathStr == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing path\"}");
return;
}
java.nio.file.Path path = McpFsAccess.normalizePath(pathStr);
if (path == null || !McpFsAccess.isAllowed(path)) {
sendJson(exchange, 403, "{\"ok\":false,\"error\":\"Path not allowed\"}");
return;
}
if (!java.nio.file.Files.isDirectory(path)) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Not a directory\"}");
return;
}
int max = parseInt(params.get("max"), 1000);
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"entries\":[");
int count = 0;
try (java.util.stream.Stream<java.nio.file.Path> stream = java.nio.file.Files.list(path)) {
java.util.Iterator<java.nio.file.Path> it = stream.iterator();
while (it.hasNext()) {
if (count >= max) {
break;
}
java.nio.file.Path entry = it.next();
if (count > 0) {
sb.append(",");
}
sb.append("{");
sb.append("\"name\":\"").append(jsonEscape(entry.getFileName().toString())).append("\"");
sb.append(",\"path\":\"").append(jsonEscape(entry.toString())).append("\"");
boolean isDir = java.nio.file.Files.isDirectory(entry);
sb.append(",\"type\":\"").append(isDir ? "dir" : "file").append("\"");
if (!isDir) {
try {
sb.append(",\"size\":").append(java.nio.file.Files.size(entry));
} catch (IOException e) {
sb.append(",\"size\":-1");
}
}
try {
sb.append(",\"mtime\":").append(java.nio.file.Files.getLastModifiedTime(entry).toMillis());
} catch (IOException e) {
sb.append(",\"mtime\":-1");
}
sb.append("}");
count++;
}
}
sb.append("]}");
sendJson(exchange, 200, sb.toString());
}
}
private class FsReadHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
Map<String, String> params = parseParams(exchange);
String pathStr = params.get("path");
if (pathStr == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing path\"}");
return;
}
java.nio.file.Path path = McpFsAccess.normalizePath(pathStr);
if (path == null || !McpFsAccess.isAllowed(path)) {
sendJson(exchange, 403, "{\"ok\":false,\"error\":\"Path not allowed\"}");
return;
}
if (!java.nio.file.Files.isRegularFile(path)) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Not a file\"}");
return;
}
long offset = parseLong(params.get("offset"), 0L);
int maxBytes = parseInt(params.get("maxBytes"), 262144);
if (maxBytes < 1) {
maxBytes = 1;
}
byte[] data;
try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(path.toFile(), "r")) {
if (offset > 0) {
raf.seek(offset);
}
int len = (int) Math.min(maxBytes, raf.length() - raf.getFilePointer());
if (len < 0) {
len = 0;
}
data = new byte[len];
int read = raf.read(data);
if (read < len) {
data = java.util.Arrays.copyOf(data, Math.max(read, 0));
}
}
String text = new String(data, StandardCharsets.UTF_8);
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"bytes\":").append(data.length).append(",\"data\":\"");
sb.append(jsonEscape(text)).append("\"}");
sendJson(exchange, 200, sb.toString());
}
}
private class FsHeadHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
handleHeadTail(exchange, true);
}
}
private class FsTailHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
handleHeadTail(exchange, false);
}
}
private void handleHeadTail(HttpExchange exchange, boolean head) throws IOException {
Map<String, String> params = parseParams(exchange);
String pathStr = params.get("path");
if (pathStr == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing path\"}");
return;
}
java.nio.file.Path path = McpFsAccess.normalizePath(pathStr);
if (path == null || !McpFsAccess.isAllowed(path)) {
sendJson(exchange, 403, "{\"ok\":false,\"error\":\"Path not allowed\"}");
return;
}
if (!java.nio.file.Files.isRegularFile(path)) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Not a file\"}");
return;
}
int lines = parseInt(params.get("lines"), 200);
if (lines < 1) {
lines = 1;
}
List<String> result = new ArrayList<String>();
if (head) {
try (java.io.BufferedReader reader = java.nio.file.Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null && result.size() < lines) {
result.add(line);
}
}
} else {
java.util.ArrayDeque<String> deque = new java.util.ArrayDeque<String>(lines);
try (java.io.BufferedReader reader = java.nio.file.Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
if (deque.size() >= lines) {
deque.pollFirst();
}
deque.addLast(line);
}
}
result.addAll(deque);
}
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"lines\":[");
for (int i = 0; i < result.size(); i++) {
if (i > 0) {
sb.append(",");
}
sb.append("\"").append(jsonEscape(result.get(i))).append("\"");
}
sb.append("]}");
sendJson(exchange, 200, sb.toString());
}
private class FsGlobHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
Map<String, String> params = parseParams(exchange);
String pattern = params.get("pattern");
if (pattern == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing pattern\"}");
return;
}
int max = parseInt(params.get("max"), 1000);
int maxDepth = parseInt(params.get("maxDepth"), 6);
java.nio.file.FileSystem fs = java.nio.file.FileSystems.getDefault();
java.nio.file.PathMatcher matcher = fs.getPathMatcher("glob:" + pattern);
List<java.nio.file.Path> roots = McpFsAccess.getAllowedRoots();
StringBuilder sb = new StringBuilder();
sb.append("{\"ok\":true,\"matches\":[");
int count = 0;
for (java.nio.file.Path root : roots) {
if (count >= max) {
break;
}
if (!java.nio.file.Files.exists(root)) {
continue;
}
try (java.util.stream.Stream<java.nio.file.Path> stream = java.nio.file.Files.walk(root, maxDepth)) {
java.util.Iterator<java.nio.file.Path> it = stream.iterator();
while (it.hasNext()) {
if (count >= max) {
break;
}
java.nio.file.Path path = it.next();
boolean matched = matcher.matches(path);
if (!matched) {
try {
java.nio.file.Path rel = root.relativize(path);
matched = matcher.matches(rel);
} catch (IllegalArgumentException e) {
matched = false;
}
}
if (matched) {
if (count > 0) {
sb.append(",");
}
sb.append("\"").append(jsonEscape(path.toString())).append("\"");
count++;
}
}
}
}
sb.append("]}");
sendJson(exchange, 200, sb.toString());
}
}
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(",\"confirmPending\":").append(owner.isSyncConfirmPending());
sb.append(",\"confirmConvenient\":").append(owner.isSyncConfirmConvenient());
sb.append(",\"buttonLabel\":\"").append(jsonEscape(owner.getSyncButtonLabel())).append("\"");
sb.append("}");
return sb.toString();
......@@ -168,6 +456,10 @@ public class McpServer {
}
private static void sendJson(HttpExchange exchange, int code, String body) throws IOException {
// codex 2026-01-27: add newline to avoid shell prompt merging with JSON
if (!body.endsWith("\n")) {
body = body + "\n";
}
byte[] data = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
......@@ -193,6 +485,28 @@ public class McpServer {
return params;
}
private static int parseInt(String value, int fallback) {
if (value == null) {
return fallback;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
return fallback;
}
}
private static long parseLong(String value, long fallback) {
if (value == null) {
return fallback;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return fallback;
}
}
private static void parseQueryString(String raw, Map<String, String> out) {
if (raw == null || raw.isEmpty()) {
return;
......@@ -209,6 +523,14 @@ public class McpServer {
}
}
private static boolean parseBool(String value, boolean defaultValue) {
if (value == null || value.isEmpty()) {
return defaultValue;
}
String v = value.trim().toLowerCase();
return v.equals("1") || v.equals("true") || v.equals("yes") || v.equals("y");
}
private static String urlDecode(String value) {
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
......
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