# MCP HTTP quick how-to (imagej-elphel)

This is a minimal HTTP interface for interacting with the GUI logic without blocking dialogs.
The MCP server runs **inside the ImageJ JVM**, so it starts/stops with the plugin.

## Start
The server auto-starts when `Eyesis_Correction` runs.

Default:
- URL: `http://127.0.0.1:48888`

Lifecycle helper script:
- `scripts/eyesis_mcp_ctl.sh` can manage process lifecycle from terminal:
  - `start` / `stop` / `restart` / `status` / `logs` / `wait`
  - example:
    ```bash
    /home/elphel/git/imagej-elphel/scripts/eyesis_mcp_ctl.sh start \
      --configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --rebuild
    ```
  - by default it uses the same VM settings as the Eclipse run profile from `AGENTS.md`
    and waits for MCP readiness.

Host daemon mode (recommended for no-repeat approvals):
- `scripts/eyesis_host_daemon_ctl.sh start` starts a queue-based host daemon in your desktop session.
- `scripts/eyesis_host_submit.sh ctl ...` and `scripts/eyesis_host_submit.sh mcp ...` submit requests through that daemon.
- details: `scripts/eyesis-host-daemon-howto.md`.

Optional JVM system properties:
- `-Delphel.mcp.port=PORT` (override port, default 48888)
- `-Delphel.mcp.mode=true|false` (enable/disable MCP dialog mode; default true)
- `-Delphel.mcp.dialogTimeoutMs=30000` (MCP dialog submit wait; 0 = wait forever)
- `-Delphel.slf4j.level=ERROR|WARN|INFO|DEBUG|TRACE` (override SLF4J/logback + JUL root level to reduce OME/Bio-Formats noise)
- `-Delphel.mcp.allowed.config=/path/to/config.corr-xml` (enable MCP file access rooted from list in config)
- `-Delphel.mcp.allowed.configdir=/path/to/configs` (allow any `*.corr-xml` in this directory)
- `-Delphel.mcp.allowed.extra=/path/a,/path/b` (comma/semicolon-separated extra allowed roots)

If `mcp.mode=false`, dialogs behave normally (GUI dialogs appear).

## Endpoints

### 1) Status
```
curl http://127.0.0.1:48888/mcp/status
```
Response (example):
```
{"running":false,"stopRequested":0,"confirmPending":false,"confirmConvenient":false,"buttonLabel":""}
```

### 2) Current dialog schema
```
curl http://127.0.0.1:48888/mcp/dialog
```
Response (example):
```
{"ok":true,"dialog":{"id":"...","title":"Set CLT parameters","fields":[...]} }
```
Each field includes:
- `type`: message | boolean | number | string | choice
- `label`: dialog label string
- `tab`: tab name
- `tooltip`: original tooltip
- `units`: numeric units (if any)
- `default`: default value as string
- `choices`: for choice fields

### 3) Set a dialog value
Set by **label** (labels must match exactly):
```
curl -X POST -d "label=Nominal (rectilinear) disparity between side of square cameras (pix)&value=12.5" \
  http://127.0.0.1:48888/mcp/dialog/values
```
Notes:
- Values are strings; numeric/boolean are parsed by the code.
- For choice fields, `value` can be an index (`0`, `1`, ...) or the exact choice text.
- If you open multiple dialogs, include the dialog `id` from `/mcp/dialog` to avoid cross-talk.

### 3b) Submit dialog (MCP “OK/Cancel”)
After setting values, submit the dialog:
```
curl -X POST -d "ok=1" http://127.0.0.1:48888/mcp/dialog/submit
```
Use `ok=0` to cancel.

With dialog id:
```
curl -X POST -d "id=...&ok=1" http://127.0.0.1:48888/mcp/dialog/submit
```

### 3a) MCP file selection (Restore/Save)
Some file pickers use MCP instead of the Swing file chooser when MCP mode is enabled. In that case a single string field
is exposed via `/mcp/dialog` with label `Selected path`. Provide it like this:
```
curl -X POST -d "label=Selected path&value=/path/to/file.corr-xml" \
  http://127.0.0.1:48888/mcp/dialog/values
```

For multi-select pickers, the field label is `Selected paths`. Use comma or newline separated paths:
```
curl -X POST -d "label=Selected paths&value=/path/a.tif,/path/b.tif" \
  http://127.0.0.1:48888/mcp/dialog/values
```

### 4) Trigger any button
```
curl -X POST -d "label=Setup CLT parameters" http://127.0.0.1:48888/mcp/button
```
This uses the same label as the GUI button.

### 5) Interrupt long-running commands
Request a stop (confirm=1 means pause and wait for confirmation):
```
curl -X POST -d "confirm=1&asap=0" http://127.0.0.1:48888/mcp/interrupt
```

Confirm or cancel a pending stop:
```
curl -X POST -d "stop=1" http://127.0.0.1:48888/mcp/interrupt/confirm
curl -X POST -d "stop=0" http://127.0.0.1:48888/mcp/interrupt/confirm
```

### 6) Read-only file access (allowed roots only)
Get allowed roots:
```
curl "http://127.0.0.1:48888/mcp/fs/roots"
```

List directory:
```
curl "http://127.0.0.1:48888/mcp/fs/list?path=/media/elphel/btrfs-data/lwir16-proc/NC"
```

Read file slice:
```
curl "http://127.0.0.1:48888/mcp/fs/read?path=/path/to/file.log&offset=0&maxBytes=16384"
```

Head/tail:
```
curl "http://127.0.0.1:48888/mcp/fs/head?path=/path/to/file.log&lines=50"
curl "http://127.0.0.1:48888/mcp/fs/tail?path=/path/to/file.log&lines=50"
```

Glob (within allowed roots):
```
curl "http://127.0.0.1:48888/mcp/fs/glob?pattern=**/*-SYSTEM_OUT.log&max=200"
```

Read a single column from a delimited file (1-based column index, default tab):
```
curl "http://127.0.0.1:48888/mcp/fs/csvcol?path=/path/to/file.csv&col=3"
curl "http://127.0.0.1:48888/mcp/fs/csvcol?path=/path/to/file.csv&col=3&sep=,"
```

### 7) RAG query (local index)
Query the local RAG index built by `scripts/rag_index.py`.

Query parameters:
- `text` (required): query string
- `topK` (optional): number of results (default 5)

```
curl "http://127.0.0.1:48888/mcp/rag/query?text=Explain%20differential%20rectification&topK=5"
```

RAG root path (optional, for non-standard checkouts):
```
-Delphel.rag.root=/home/elphel/wksp2/imagej-elphel
```

Example (config directory):
```
-Delphel.mcp.allowed.configdir=/media/elphel/btrfs-data/lwir16-proc/NC/config
```

Note: list parsing currently calls `getSourceSets(...)`, which has `mkdirs()` side-effects (marked with `// codex 2026-01-28`).
If you want MCP to be strictly read-only, consider removing or guarding those `mkdirs()` calls.

## Behavior notes
- MCP dialogs **do not block** in this version. If MCP doesn’t set a value, defaults are used.
- `Abort` and `Stop` labels are handled specially (set stopRequested).

## Troubleshooting
- If a dialog field isn’t found, check the exact label in `/mcp/dialog` output.
- If the server doesn’t respond, verify the plugin is running and the port is open.
- MCP startup log line `MCP: local-only (no reverse SSH tunnel detected; see mcp-remote-ssh-tunnel.md)` indicates a reverse tunnel is not running.

## Claude Code — native usage notes

Claude Code runs directly on the host (not sandboxed), so it can call MCP HTTP endpoints
with plain `curl` in `Bash` without going through the host daemon.  The daemon is still
needed for **desktop-session operations** (starting/stopping the ImageJ process), because
those need to run in the graphical session where the user is logged in.

### Recommended pattern for Claude

| Task | How |
|------|-----|
| Read status / dialog / log | Direct `curl http://127.0.0.1:48888/...` in Bash |
| Press a button / set dialog value / submit | Direct `curl -X POST ...` in Bash |
| Start / stop ImageJ | `eyesis_host_submit.sh ctl start|stop` via the host daemon |
| Monitor a long run autonomously | `ScheduleWakeup` (built-in Claude tool) |

### Why direct curl for MCP reads (not the daemon)

The daemon adds queue round-trip latency (~1–2 s per call).  For a multi-step dialog
sequence (button → check dialog appeared → set value → submit) this matters: if a step
takes too long the dialog auto-timeout fires (`dialogTimeoutMs`, default unlimited but
depends on JVM args) and the next step gets a 409.  With direct curl the round-trip is
<50 ms and the sequence completes atomically.

**Always include the dialog `id`** when setting values and submitting, to guard against
a dialog changing between the `GET /mcp/dialog` and the `POST /mcp/dialog/values` calls:

```bash
DLG_ID=$(curl -s http://127.0.0.1:48888/mcp/dialog | python3 -c "import sys,json; print(json.load(sys.stdin)['dialog']['id'])")
curl -sS -X POST \
  --data-urlencode "id=$DLG_ID" \
  --data-urlencode "label=Selected path" \
  --data-urlencode "value=/path/to/file.corr-xml" \
  http://127.0.0.1:48888/mcp/dialog/values
curl -sS -X POST \
  --data-urlencode "id=$DLG_ID" \
  --data-urlencode "ok=1" \
  http://127.0.0.1:48888/mcp/dialog/submit
```

### Full autonomous run sequence (Claude)

```bash
# 1. Ensure host daemon is running (needed for ctl start)
bash scripts/eyesis_host_daemon_ctl.sh status || bash scripts/eyesis_host_daemon_ctl.sh start

# 2. Start ImageJ (blocks until MCP is up)
EYESIS_HOST_WAIT_SEC=300 EYESIS_HOST_TIMEOUT_SEC=600 \
  bash scripts/eyesis_host_submit.sh ctl start \
  --configdir /home/elphel/lwir16-proc/LV/config

# 3. Press Restore
curl -sS -X POST --data-urlencode "label=Restore" http://127.0.0.1:48888/mcp/button
sleep 1

# 4. Get dialog id, set config path, submit
DLG_ID=$(curl -s http://127.0.0.1:48888/mcp/dialog | python3 -c "import sys,json; print(json.load(sys.stdin)['dialog']['id'])")
curl -sS -X POST --data-urlencode "id=$DLG_ID" \
  --data-urlencode "label=Selected path" \
  --data-urlencode "value=/path/to/config.corr-xml" \
  http://127.0.0.1:48888/mcp/dialog/values
curl -sS -X POST --data-urlencode "id=$DLG_ID" --data-urlencode "ok=1" \
  http://127.0.0.1:48888/mcp/dialog/submit
sleep 3

# 5. Start processing
curl -sS -X POST --data-urlencode "label=Aux Build Series" http://127.0.0.1:48888/mcp/button

# 6. Monitor log via MCP fs/tail (works even outside normal sandbox permissions)
LOG="/home/elphel/lwir16-proc/LV/models/models_1773135103-1773135664/1773135590_524726/1773135590_524726-SYSTEM_OUT.log"
curl -s "http://127.0.0.1:48888/mcp/fs/tail?path=${LOG}&lines=30"
```

### Log monitoring via MCP fs

The `/mcp/fs/tail` endpoint is the right way to read log files from Claude — it works
within the MCP allowed roots without needing direct filesystem permissions:

```bash
# Tail last 50 lines
curl -s "http://127.0.0.1:48888/mcp/fs/tail?path=/path/to/file.log&lines=50"

# Check allowed roots first if unsure whether a path is accessible
curl -s "http://127.0.0.1:48888/mcp/fs/roots"
```

The allowed roots are derived from the config loaded at startup (`-Delphel.mcp.allowed.configdir`
causes the program to expand all directories referenced in the config).  The model output
directories (`models/...`), linked directories, results, and config are typically all included.

### Interrupt and stop

```bash
# Graceful stop (asap=1 = stop at next convenient checkpoint)
curl -sS -X POST --data-urlencode "confirm=0" --data-urlencode "asap=1" \
  http://127.0.0.1:48888/mcp/interrupt

# If MCP is unresponsive (e.g. stuck in infinite loop), kill via daemon:
bash scripts/eyesis_host_submit.sh ctl stop
```

### Autonomous monitoring with ScheduleWakeup

Claude has a `ScheduleWakeup` tool that fires a follow-up prompt after a delay.
This is the right mechanism for "check the log in 30 minutes" tasks:

- Schedule at 20 min for an early-catch check before the expected hang point
- Schedule at 30 min as a fallback
- Both can be active simultaneously
- Delays under 300 s keep the prompt cache warm; over 300 s incurs a cache miss

---

## Forward plan (single host → multi-host)
- **Single host first**: keep MCP local and use it to supervise runs and inspect outputs.
- **Health checks**: add a small run‑state file (timestamp, last scene, ok/error) and/or MCP status summary.
- **Work units**: define a scene‑level boundary so each unit is independent and outputs don’t conflict.
- **Idempotent commands**: prefer “process scene X with config Y” so retries are safe.
- **Distributed later**: add a lightweight work queue or task‑stealing in the program; MCP stays for monitoring each host.
- **Supervision**: codex can watch CSV/log indicators and stop/restart on anomalies.

## Notes: mkdirs side-effects in list parsing
`EyesisCorrectionParameters.CorrectionParameters.getSourceSets(...)` creates directories as part of parsing.
These calls were marked with `// codex 2026-01-28`:
- `rootDirectory` resolved base dir `mkdirs()`
- `sourceDirectory` `mkdirs()`
- per-`KEY_DIRS` (linkedModels, linkedCenters, videoDirectory, x3dDirectory, resultsDirectory, cuasSeedDir) `mkdirs()`

If you want to remove side-effects for MCP-only parsing, consider guarding or removing those `mkdirs()` calls.

## Quick test sequence (minimal MCP flow)

1) **Start in MCP mode**
   - In Eclipse VM args, remove `-Delphel.mcp.mode=false` (or set `-Delphel.mcp.mode=true`).

2) **Check server is up**
```
curl http://127.0.0.1:48888/mcp/status
```

3) **Trigger a dialog**
   Use MCP to click a button that normally opens a dialog (example):
```
curl -X POST -d "label=Setup CLT parameters" http://127.0.0.1:48888/mcp/button
```
   In MCP mode, no GUI dialog appears; schema should be published.

4) **Fetch dialog schema**
```
curl http://127.0.0.1:48888/mcp/dialog
```

5) **Set a value by label**
```
curl -X POST -d "label=Nominal (rectilinear) disparity between side of square cameras (pix)&value=12.5" \
  http://127.0.0.1:48888/mcp/dialog/values
```

6) **Run a command**
```
curl -X POST -d "label=CLT batch process" http://127.0.0.1:48888/mcp/button
```

If a value does not take, re-check exact label text via `/mcp/dialog`.
- `-Delphel.ome.singleton=true|false` (toggle OMEXMLService singleton reuse; default false)
