summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md277
1 files changed, 277 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md
new file mode 100644
index 0000000..a3b6111
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md
@@ -0,0 +1,277 @@
+# 20 — MCP server (green-eye agent interface)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. What it is
+
+`plugins/mcp_server.py` is the green-eye **MCP (Model Context Protocol)
+server** — roadmap [`R3`](08-ROADMAP.md#r3-mcp-server-plugin) and
+[`H7`](../GREEN-EYE-ROADMAP.md). It exposes the *active* `Session` as MCP
+tools so an AI agent (Claude Code, opencode, any MCP client) can inspect
+symbols, set breakpoints/watchpoints, run the target, read memory, snapshot,
+analyse/compare recordings, inject services, and drive the harnesses.
+
+* **Stdlib-only**: JSON-RPC 2.0 over newline-delimited stdin/stdout, no MCP
+ library, no network listener unless explicitly asked.
+* **Plugin first**: `register(ge)` adds an `mcp-serve` REPL/CLI command that
+ serves the session already loaded by green-eye.
+* **Standalone too**: the same file runs as a script that launches its own
+ session (`--kernel`, `--backend`, `--record`) and then serves MCP.
+* **Read-only by default**: memory writes, remote calls and injection require
+ explicit startup flags.
+* **Defensive capabilities**: tools owned by other components (mutation and
+ portability harnesses, native trace, injection) return a structured
+ `not_available` tool error when the module/file is absent instead of
+ crashing. The tool list always advertises every capability.
+
+The core green-eye package is untouched; everything lives in the plugin.
+
+## 2. Running it
+
+All commands run from `open-iov/debug/risc-v-gsp-rm-vm/`.
+
+### 2.1 Standalone (the usual MCP client mode)
+
+```bash
+# bundled demo, QEMU, recorded session
+python3 plugins/mcp_server.py --stdio --record
+
+# native simulator, recorded
+python3 plugins/mcp_server.py --stdio --backend native --kernel firmware/uproc_demo/build/uproc_demo.elf --record
+
+# enable writes / injection (never on by default)
+python3 plugins/mcp_server.py --stdio --allow-write --allow-inject
+
+# local loopback TCP instead of stdio (for clients that cannot spawn a process)
+python3 plugins/mcp_server.py --tcp 127.0.0.1:8765 --record
+```
+
+Flags: `--stdio` (default), `--tcp HOST:PORT` (loopback only),
+`--kernel ELF`, `--backend qemu|native`, `--record [PATH]`, `--name NAME`,
+`--plugins PATH` (repeatable), `--allow-write`, `--allow-inject`.
+Logs go to stderr; **stdout carries protocol frames only**.
+
+### 2.2 Plugin mode (serve a live session)
+
+```bash
+./greeneye.sh attach --kernel firmware/uproc_demo/build/uproc_demo.elf \
+ --plugin plugins/mcp_server.py
+greeneye> mcp-serve # read/observe
+greeneye> mcp-serve --allow-write # + memory writes / remote calls
+greeneye> mcp-serve --allow-inject # + service injection
+```
+
+`mcp-serve` blocks until the MCP client disconnects or calls the `close`
+tool, then returns to the REPL. In plugin mode `close` stops the server but
+**does not** close the session (the REPL still owns it); in standalone mode
+`close` also closes the launched session.
+
+Both `--stdio` and `--tcp` behave identically. The bundled plugin directory is
+loaded by default, so `mcp_server.py` is auto-discovered; the command only
+serves when invoked.
+
+## 3. Client configuration
+
+The MCP server must be started **before** the session it should serve, so
+client configs use standalone mode. Use absolute paths.
+
+### 3.1 Claude Code / `.mcp.json`
+
+```json
+{
+ "mcpServers": {
+ "green-eye": {
+ "command": "python3",
+ "args": [
+ "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py",
+ "--stdio",
+ "--backend", "native",
+ "--kernel", "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/firmware/uproc_demo/build/uproc_demo.elf",
+ "--record"
+ ]
+ }
+ }
+}
+```
+
+Or, one line: `claude mcp add green-eye -- python3
+<tool-root>/plugins/mcp_server.py --stdio --record`.
+
+### 3.2 opencode (`opencode.json`)
+
+```json
+{
+ "$schema": "https://opencode.ai/config.json",
+ "mcp": {
+ "green-eye": {
+ "type": "local",
+ "command": [
+ "python3",
+ "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py",
+ "--stdio", "--backend", "native", "--record"
+ ],
+ "enabled": true
+ }
+ }
+}
+```
+
+For an agent that must write memory or inject, add `--allow-write` /
+`--allow-inject` to `args` and keep the session recorded (the recording is
+the audit trail).
+
+## 4. Protocol
+
+| Request | Behaviour |
+|---|---|
+| `initialize` | returns `protocolVersion` (echoes `2024-11-05`, `2025-03-26` or `2025-06-18`; otherwise the server default), `serverInfo`, `capabilities.tools` |
+| `notifications/initialized` | accepted silently (notifications never get a response) |
+| `ping` | `{}` |
+| `tools/list` | the 24-tool catalogue with JSON input schemas |
+| `tools/call` | `{"name": ..., "arguments": {...}}` → `{"content":[{"type":"text","text":"<json>"}]}`, or `isError: true` for tool errors |
+
+JSON-RPC errors: `-32700` parse error, `-32600` invalid request, `-32601`
+unknown method, `-32602` unknown tool / bad params. Tool-level failures
+(bad address, symbol not found, gating, capability absent) are returned as
+`isError: true` results whose text is a JSON object with `error` and
+`message` — the server never dies on a bad call.
+
+Tool result payloads are always a single JSON text block so agents can parse
+them deterministically.
+
+## 5. Tool catalogue
+
+| Tool | Arguments | Class | Notes |
+|---|---|---|---|
+| `session_info` | — | read | backend, PC, symbols, recording, breakpoints/watches, enabled capabilities |
+| `symbols_resolve` | `target` | read | symbol, `symbol+0xNN` or hex address |
+| `bp_set` | `target`, `kind=sw\|hw` | observe | hits recorded as `bp_hit` |
+| `bp_clear` | `target` | observe | removes every breakpoint at that address |
+| `watch_set` | `target`, `size=8`, `kind=w\|r\|a` | observe | hits recorded as `watch_hit` |
+| `run` | `duration=1.0`, `max_stops`, `timeout` | observe | bounded; services hooks/watchpoints |
+| `run_until` | `target`, `timeout=5.0` | observe | temporary breakpoint |
+| `step` | `count=1` (max 10000) | observe | dispatches hooks/watchpoints |
+| `read_mem` | `addr`, `size=64` (max 1 MiB) | read | lowercase hex |
+| `write_mem` | `addr`, `hex` | **write** | requires `--allow-write`; recorded as `mem_write` |
+| `hexdump` | `addr`, `size=64` (max 1 MiB) | read | symbol-annotated text |
+| `snapshot` | `addr`, `size` (max 16 MiB), `label=mcp` | read | zlib + sha256 into the session |
+| `call_function` | `target`, `args=[]`, `timeout=5.0` | **write** | requires `--allow-write`; Frida-like remote call |
+| `inject_service` | `service`, `slot=6`, `load_addr`, `cmd=1`, `arg0`, `arg1`, `path=remote\|firmware\|both`, `settle=0.25` | **inject** | requires `--allow-inject`; service ELF built with `make sdk` |
+| `trace_start` | `exec=true`, `start`, `end`, `max_events` | trace | native backend + recording session only |
+| `trace_stop` | — | trace | flushes `blobs/trace.jsonl`, returns counts |
+| `analyze_current` | `max_rows=40` | read | `render_summary` for the active recording |
+| `compare_sessions` | `a`, `b`, `symbols=true`, `max_rows=40` | read | A/B recorded sessions |
+| `export_hits` | `session`, `out` | read | Ghidra `greeneye_hits.tsv` |
+| `probe_firmware` | `elf`, `instructions=2000`, `duration=1.0`, `no_relocate`, `symbols`, `timeout=300` | harness | QEMU-only probe subprocess; first divergence |
+| `mutation_run` | `kernel`, `out`, `iterations=10`, `seed=1`, `timeout=0.25`, `settle=0.2`, `memory_region`, `allow_foreign_elf` | harness | `harness/mutation_harness.py`, QEMU-only |
+| `portability_matrix_run` | `targets[]`, `out`, `probe_instructions=2000`, `scenario_duration=0.4`, `symbols`, `max_rows` | harness | `harness/portability_matrix.py`, QEMU-only |
+| `plugins_list` | — | read | bundled plugins, `GREENEYE_PLUGINS`, REPL commands |
+| `close` | — | session | stop serving; standalone also closes the session |
+
+Capability classification follows the manifest in
+[15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5: read / observe are
+unattended-safe; write and inject require the explicit startup flags and a
+recorded session. `bp_set`, `watch_set`, `run` and `step` mutate target
+*observation state* only and stay in the observe class.
+
+Harness and probe tools shell out to the existing CLIs with bounded budgets
+and return their stdout/stderr; they never touch hardware, a guest or a
+kernel module.
+
+## 6. Example agent sessions
+
+### 6.1 Monitor an automated firmware-release run
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"session_info","arguments":{}}}
+{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"symbols_resolve","arguments":{"target":"rpc_ping"}}}
+{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"bp_set","arguments":{"target":"rpc_ping"}}}
+{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run","arguments":{"duration":0.5,"max_stops":100}}}
+{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"analyze_current","arguments":{"max_rows":20}}}
+```
+
+The agent watches `bp_hit`/`watch_hit` counts in the report, snapshots the
+control block, runs the release stimulus, then diffs the new recording
+against the golden one:
+
+```json
+{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"gsp_ctrl","size":392,"label":"release"}}}
+{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"compare_sessions","arguments":{"a":"dumps/golden","b":"dumps/release","symbols":true}}}
+```
+
+### 6.2 Dump memory
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hexdump","arguments":{"addr":"0x80000840","size":128}}}
+{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_mem","arguments":{"addr":"0x80000840","size":16}}}
+{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"0x80000000","size":4096,"label":"ram-window"}}}
+```
+
+### 6.3 Inject a service
+
+Start the server with `--allow-write --allow-inject --record`, then:
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"inject_service","arguments":{"service":"sdk/build/injected_demo.elf","slot":6,"cmd":1,"arg0":65,"arg1":66,"path":"both"}}}
+```
+
+The result contains the load address/entry and both the direct (`remote`) and
+firmware-mailbox (`firmware`) return values; every segment write and call is
+recorded, and the session can be compared like any other run.
+
+## 7. Security rules
+
+1. **Local only.** Default transport is stdin/stdout. `--tcp` binds loopback
+ addresses only (`127.0.0.1`, `localhost`, `::1`); any other host is
+ refused at argument parse time. There is no auth, so never expose it.
+2. **No guest, no hardware.** The server drives the simulator/QEMU session
+ only. It does not touch `/dev`, a kernel module, an mdev, a guest or a
+ physical GPU; the agent runs against the simulator by default
+ ([H7](../GREEN-EYE-ROADMAP.md), [15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5).
+3. **Read-only by default.** `--allow-write` enables `write_mem` and
+ `call_function`; `--allow-inject` enables `inject_service`. The agent's
+ client config decides which manifest class it gets; a read/observe agent is
+ configured without those flags.
+4. **Sessions are the audit trail.** Writes, calls, injections and snapshots
+ land in `events.jsonl`; run with `--record` (the trace tools require it).
+ A finding without a session is not a finding.
+5. **Budgets everywhere.** `run` defaults to 1 s, `step` is capped, reads and
+ snapshots have size caps, and harness subprocesses have timeouts. A wedged
+ target is left stopped and restarted clean, never poked.
+6. **Protocol hygiene.** stdout is reserved for JSON-RPC; diagnostics go to
+ stderr. One bad tool call returns `isError` and the server keeps serving.
+7. **No product changes.** MCP can propose evidence, not merge emulation
+ changes; human approval still gates `open-iov-vfio`.
+
+## 8. Absent capabilities
+
+`probe_firmware`, `mutation_run`, `portability_matrix_run`, `trace_*`,
+`inject_service`, `analysis` and the Ghidra export are checked at call time.
+If a module/file is missing the tool returns:
+
+```json
+{"content":[{"type":"text","text":"{\"error\": \"not_available\", \"message\": \"mutation harness not present: ...\"}"}],"isError": true}
+```
+
+so an agent can plan around a capability instead of seeing the server crash.
+The tool list is stable regardless of what is present.
+
+## 9. Testing
+
+```bash
+python3 -m unittest tests.test_mcp -v # 34 tests, no external deps
+```
+
+The suite drives `initialize`/`tools/list`/`tools/call` over in-memory
+streams (including unknown method/tool, parse errors, write/inject gating,
+no-stdout-pollution and structured `not_available`), plus a standalone
+subprocess test on the native backend that skips cleanly when the native
+simulator is absent.
+
+## 10. Roadmap
+
+* **R3 (done)** — MCP plugin exposing sessions as agent tools; local stdio,
+ no network by default; capability gating.
+* **H7 remaining** — named harness entry points and budgets, a session index
+ for agents, enforced capability manifest files, and the physical-escalation
+ gate. Those build on this server, not around it.