PSX RiPPER home

MCP: an AI agent controls the app

In short: MCP (glossary) lets an AI agent on your computer use the app over your home network, the same way you would with the pad. It is off until you turn it on. This page shows how to turn it on, how to connect an agent, and every command (tool) the agent can use.

With MCP on, an AI agent such as Claude Code can read what PSX RiPPER shows and control it over your home network: start a rip, follow it, read the log, change a setting, check and tidy the game list, read and undo the audit log. Everything the app can do has a tool of its own; the buttons are only needed to answer a dialog. MCP is the Model Context Protocol (opens in a new tab); the app is an MCP server at http://<the PS3's address>:8090/mcp.

Turning it on

START opens the settings. On Remote control, opens a menu with three switches; ← → or turn the selected one on or off:

Row What it does
Remote view The screen and a pad in a web browser: http://<ps3>:8090/
MCP An agent can read and control the app: http://<ps3>:8090/mcp
status.json The drives' state as JSON for other programs: http://<ps3>:8090/status.json

The line at the bottom shows the console's address for the selected row. MCP is off until you turn it on. While it is on, [MCP] is shown in yellow at the left of the title on every screen, so you can always see that an agent may be at work. Every setting change, rip, repair, check and game-list change an agent makes is in the audit log with the source MCP; settings, renames and moves to and from the trash can be undone there (by you, or by the agent with audit_undo), as when you make them. Emptying the trash cannot be undone, and a rip is logged as a job, not undone. cancel, skip_rereads, eject, a Redump (glossary) download and a webhook (glossary) test are only in the app's log (psxrip.log), as they are from the pad.

Connecting an agent

In Claude Code:

claude mcp add --transport http psx-ripper http://192.168.1.50:8090/mcp

Use your console's address (the Remote control menu shows it). Other MCP clients work the same way: the transport is Streamable HTTP, one JSON request per POST, no login.

If MCP is off on the console, the agent gets the answer MCP is off on the console. Turn it on in PSX RiPPER: Settings → Remote control → MCP.

Under the hood every call is one JSON-RPC 2.0 request, so curl works too:

curl -s -X POST http://192.168.1.50:8090/mcp -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"get_drives","arguments":{}}}'

The examples below show the arguments and the text the tool answers with. They come from the simulator (make -C tests/host remote) and are shortened where … stands.

How the tools work

  • Reading tools (get_*, list_settings, read_log, list_games, get_game, audit_log, preview_name) never touch the screen.
  • Actions (rip, check, …, reset_settings, redump_download, audit_undo, audit_redo and the game tools) do the work the way the pad would, with the same checks, and answer with what was done and the screen state afterwards, or with the reason they were refused (in English, the app's own text). set_setting answers with the setting's new value, webhook_test with the HTTP status.
  • show_games and show_game show the game list, or a game's details, on the screen, and answer with what they show and the screen state.
  • go_screen, go_tab and press_button move around the app as the pad does. The console's screen moves with them: whoever is in front of the TV sees what the agent does.

A refusal comes back as a tool error (isError: true) with the text; a wrong argument (an unknown drive name, a missing argument) as a JSON-RPC error that names the argument.

Reading tools

get_state

What the screen shows: the screen's name, the selected row, the tab on the home screen, an open dialog, whether the on-screen keyboard is up, the buttons in the hint bar, busy (a drive is in use) and every drive as get_drives gives it. No arguments.

{"screen":"home","row":null,"tab":"internal","dialog":null,"keyboard":false,
 "buttons":[{"button":"cross","label":"Rip"},{"button":"triangle","label":"Eject"},
            {"button":"select","label":"Device settings"}],
 "busy":false,"drives":[{"drive":"internal","id":"internal","model":"PS3 Blu-ray drive","state":"ready",…}]}

While a Redump download runs (screen is redump_download) the state has its progress; each phase, the download and then the index, goes from 0 to 100 %:

"redump_download":{"phase":"download","percent":25,"kb":1024,"kb_total":4096,"cancelling":false}

While an undo of the audit log runs (screen is audit_undo), what it works on, whether it redoes, and how far, in MB (step is writing the sectors (glossary), then checking the rip again):

"audit_undo":{"what":"Repair","redo":false,"step":"writing","mb":12}

With a dialog open, screen is dialog and dialog says what it asks and over which screen:

{"screen":"dialog","row":0,"tab":"internal",
 "dialog":{"text":"Reset all settings to their defaults?\n\nThe webhook address and the local Redump copy are kept.",
           "question":true,"over":"settings"},
 "keyboard":false,"buttons":[{"button":"cross","label":"Yes"},{"button":"circle","label":"No"}],…}

get_drives

Every drive with its disc and job, as /status.json has them. id is the name the other tools take: internal, or usbN with the USB port. free_gb is the free space at the drive's destination, and a running job has its events, the lines its tab lists (newest first, in English). No arguments.

{"busy":false,"drives":[
 {"drive":"internal","id":"internal","model":"PS3 Blu-ray drive","state":"ready",
  "title":"Final Fantasy IX (Disc 1)","serial":"SLES_029.65","size_mb":706,
  "redump_name":"Final Fantasy IX (Europe) (Disc 1)","redump":"unchecked","already_ripped":false,
  "known_errors":0,"destination":"/dev_hdd0/PSXISO","file":"Final Fantasy IX (Disc 1) [SLES_029.65]","free_gb":338},
 {"drive":"usb","id":"usb0","port":0,"model":"Apple SuperDrive","state":"ready","title":"Croc 2",…}]}

get_screenshot

A JPEG of the app's screen at half size, as the remote view shows it, given as an MCP image (image/jpeg). System dialogs and the on-screen keyboard are drawn by the console on top of the app and are not in it. No arguments.

read_log

Lines of the app's log, /dev_hdd0/tmp/psxrip.log.

Argument Meaning
from_line The first line, 0 = the log's first. Left out: the last lines
max_lines At most 500, default 100

{"max_lines":6}:

{"lines":["mcp: setting speed=2","mcp: drive usb0: eject=0","mcp: setting webhook set",
          "game list all: 3 disc(s), 3 row(s)","eject pressed on tab 1 (USB) by mcp",
          "eject: card 1 -> usb drive 0 (port 0)"],
 "from":0,"next":6,"total":6}

next is the from_line for the next call. Where the current webhook address, its path or a header value appears in a line, the line shows [secret] instead. The webhook's host is not secret and is shown as is (the app logs webhook address set (host)). Only the current values are known: a line written with an earlier address is not changed (the app never logs the whole address). A line is cut after 1023 bytes, and when the lines do not fit in one answer it ends early with "truncated":true.

list_settings

Every setting with its key, its label in English and Swedish, its value, the allowed values (each with its labels) and its scope: standard, or a drive (internal, usb0, …) for that drive's own settings. Free-text settings (name_pattern, the webhook address and headers) have "values":null. A secret setting has "secret":true and is_set instead of its value. No arguments.

{"settings":[
 {"key":"speed","scope":"standard","label":{"en":"Read speed","sv":"Läshastighet"},"secret":false,"value":"0",
  "values":[{"value":"3","label":{"en":"2×","sv":"2×"}},{"value":"2","label":{"en":"4×","sv":"4×"}},
            {"value":"1","label":{"en":"8×","sv":"8×"}},{"value":"0","label":{"en":"Max","sv":"Max"}}]},
 {"key":"mcp","scope":"standard","label":{"en":"Remote control · MCP","sv":"Fjärrstyrning · MCP"},"secret":false,"value":"1",…},
 {"key":"webhook","scope":"standard","label":{"en":"Webhooks · Address","sv":"Webhooks · Adress"},"secret":true,"is_set":false,"values":null},
 {"key":"eject","scope":"usb0","label":{"en":"Eject the disc when done",…},"secret":false,"value":"-1",
  "values":[{"value":"-1","label":{"en":"Standard (On)","sv":"Standard (På)"}},…]},
 …]}

The keys are those of the config file, with three named differently here: each webhook event is a key of its own (webhook_event_rip_finished, …, on/off; the file keeps them together in webhook_events), webhook_progress is off or the step (the file's webhook_events and webhook_step), and a drive's destination is destination with the drive's scope (the file's outdir and outdir_usbN); values lists the destinations there are now. The game list's source and filter are not settings here: the game tools take them as arguments.

Screens and buttons

set_setting

Changes a setting as its screen would: the same checks and side effects, saved at once, logged in the audit log. Answers with the setting as list_settings gives it.

Argument Meaning
key The key, as list_settings gives it
value One of the allowed values, or any text for a free-text setting. 4 and "4" are the same; true/false are 1/0
drive For a drive's own setting; left out: the standard

{"key":"speed","value":2} → {"key":"speed","scope":"standard",…,"value":"2",…}

{"key":"eject","value":false,"drive":"usb0"} → {"key":"eject","scope":"usb0",…,"value":"0",…}

{"key":"speed","value":"5"} → refused: Not an allowed value for speed: "5". Allowed: 3 (2×), 2 (4×), 1 (8×), 0 (Max).

A secret (the webhook address, a header) can be set but is never shown: {"key":"webhook","value":"http://192.0.2.10:8123/api/webhook/example-id"} → {"key":"webhook",…,"secret":true,"is_set":true,"values":null}.

A smaller audit_mb deletes the oldest kept sector copies at once, as the settings row does: the changes they belong to can then never be undone.

Setting mcp to 0 turns MCP off: the call is answered, and the next one gets MCP is off. So does (reset everything) on the console's pad: it turns all three Remote control switches off. reset_settings, and and yes pressed with press_button, keep them.

go_screen

Opens one of the app's screens the way the pad goes there, and answers with the state.

Argument Meaning
screen home, settings, drive_settings, filename, webhooks, about, audit_log, remote_control, games
drive Only with drive_settings, and required there

{"screen":"drive_settings","drive":"usb0"}:

{"screen":"drive_settings","row":0,"tab":"usb0","drive":"usb0","dialog":null,"keyboard":false,
 "buttons":[{"button":"updown","label":"Select"},{"button":"leftright","label":"Change"},
            {"button":"triangle","label":"All standard"},{"button":"circle","label":"Back"}],…}

go_tab

Shows a drive's tab on the home screen, going there first. {"drive":"internal"} → {"screen":"home","row":null,"tab":"internal",…}. A drive that is not connected: No drive "usb3" is there now; get_drives lists the drives.

press_button

Presses one button, as the pad would, and answers with the state. It is the fallback: for answering a dialog and moving around a screen. Every action the screens offer has a tool of its own: use those for the work itself.

Argument Meaning
button cross, circle, triangle, square, up, down, left, right, start, select, l1, r1, l2, r2

{"button":"r1"} on the home screen → {"screen":"home","row":null,"tab":"usb0",…}

{"button":"circle"} on the home screen is refused: Circle on the home screen exits the app; MCP does not exit it. Use the controller.

Drive actions

rip, repair, check, cancel, skip_rereads and eject each take one argument, drive (internal or usbN), and do what the button on that drive's tab does, with the same checks. The app shows the drive's tab first. Each answers with the state, or why not.

Tool Does Pad
rip Rips the disc. It replaces an existing rip at the destination without asking, as does
repair Re-reads the sectors the rip log lists as bad
check Checks the drive's earlier rip against Redump from the file
cancel Cancels the drive's running job. The call is the confirmation the pad asks for
skip_rereads Ends the slow re-reads; the rip keeps what it has
eject Ejects the disc (not while a job runs on it)

rip {"drive":"internal"} → the state, {"screen":"home","row":null,"tab":"internal",…}; the drive's state is then ripping and its job shows the progress.

Refusals, for example:

  • repair with no rip: There is no rip of this disc at the drive's destination.
  • cancel with nothing running: Nothing runs on this drive.
  • rip while a dialog is open: A dialog is open: "…". Answer it with press_button: cross = yes, circle = no.

Follow a rip with get_drives or get_state: job has the phase and percentages, last the result when it is done.

Settings and the app

get_app_info

What About in the settings shows, the console's address, whether drives can rip at the same time, and the local Redump copy. No arguments. Reading only.

{"version":"1.4.0","about":{"description":"Rips your own PlayStation 1 discs to bit-perfect .bin/.cue images, …",
  "author":"Björn Wiberg","email":"b@bjrn.nu","source":"github.com/bjornwiberg/psx-ripper",
  "libraries":"PSL1GHT (PS3 SDK) · zlib · …","license":"Free software under the GNU General Public License v3.",
  "note":"Not affiliated with Sony Interactive Entertainment. Only rip discs you own."},
 "address":"http://192.168.1.50:8090","mcp":"http://192.168.1.50:8090/mcp","parallel_rips":true,
 "redump_copy":{"discs":21000,"version":"2026-11-01 10-00-00","fetched":"2026-11-01T10:12:00Z","downloading":false,
  "last_download":{"result":"done","discs":21000,"error":null,"version":"2026-11-01 10-00-00",
                   "at":"2026-11-01T10:12:00Z","source":"mcp"}}}

redump_copy.discs is 0 (and version null) when there is no copy. last_download is null until a download has run since the app started; result is done, failed (with the error code) or cancelled, and source says who started it (pad, remote, mcp).

preview_name

The file names a file name pattern gives, as the File name view's preview shows them, without changing anything. Use it to try a pattern before set_setting name_pattern or rename_game.

Argument Meaning
pattern %T title, %N Redump's name, %D disc ID, %R region, other characters as they are (at most 95 bytes). Empty: %T [%D]
drive Only this drive's disc. Left out: every drive with an identified disc, or an example disc when none has one
id A game (not with drive): each of its discs' new name, as the rename view shows it

{"pattern":"%T (%R)"}:

{"pattern":"%T (%R)","names":[
 {"drive":"internal","disc":"Alundra","serial":"SLES_011.35","file":"Alundra (Europe).bin","webman":"Alundra (Europe)","example":false},
 {"drive":"usb0","disc":"Croc 2","serial":"SLES_020.88","file":"Croc 2 (Europe).bin","webman":"Croc 2 (Europe)","example":false}]}

webman is the name webMAN (glossary)'s game list shows (text in [ ] is left out). With id: {"pattern":"%T","id":"hdd:/…/Alundra (Europe) [SLES_011.35]","name":"Alundra (Europe)", "discs":[{"id":…,"now":"Alundra (Europe) [SLES_011.35].bin","new":"Alundra.bin"}],"same":false}; same is true when every disc would keep its name (rename_game would refuse).

reset_settings

Sets settings back to their defaults, as does.

Argument Meaning
drive That drive's own settings go back to the standard, as All standard in its settings; its destination stays. Refused while a job runs on the drive
confirm Without drive: every setting, as and yes in the settings. Must be true, or nothing is reset

Every setting goes back to its default: the drives' standard values, the file name pattern, Redump off, webhooks off, a progress step of 5 % and 256 MB of sector copies. The webhook address, headers and events, the local Redump copy and the drives' own settings and destinations are kept. Over MCP the Remote control switches are kept too, so MCP stays on and the agent keeps its connection; on the console's pad turns them off. Each change is a row in the audit log with the source MCP and can be undone there. The answer lists the settings that were reset, as list_settings gives them, and the screen state (the settings, or the drive's settings):

{"result":{"reset":"all","note":"Every setting is back to its default. Kept: …",
  "settings":[{"key":"speed","scope":"standard",…,"value":"0",…},…]},
 "state":{"screen":"settings",…}}

Without drive and confirm: Not reset: without drive every setting goes back to its default (…). Call reset_settings again with confirm: true to reset them.

redump_download

Downloads Redump's list of PS1 discs to the console and indexes it, the local copy that the setting Redump: Local copy checks against, as on Redump local copy does. It answers at once:

{"result":{"started":true,"note":"The download runs: get_state shows how far (redump_download), get_app_info the result."},
 "state":{"screen":"redump_download",…,"redump_download":{"phase":"download","percent":0,…}}}

Follow it with get_state. When it ends the settings show again and no dialog waits on the TV (started from the pad, the result is a dialog as before): get_app_info has the result in redump_copy.last_download. A failed download keeps the copy there was. While it runs, every other request is refused at once with what is going on.

Argument Meaning
cancel true stops the download that runs, as does: {"result":{"cancelled":true},…}

webhook_test

Sends a test event ("event":"test") to the webhook address, with the headers when they are on, as Test in the webhook settings does. No arguments. It runs on a thread of its own and waits at most 4 seconds, so the screen never waits for it. The answer never shows the address or the headers:

  • {"sent":true,"status":204,"ok":true}: the receiver answered (ok for a 2xx status).
  • {"sent":false,"status":null,"error":"The address's host name could not be resolved."}: it failed; other errors are not an http:// address, could not be reached and did not answer.
  • {"sent":false,…,"error":"No answer within 4 s: …"}: the test goes on in the background (read_log shows webhook test -> status (mcp) when it ends); a new test is refused until then.

Refused when no address is set, and while a dialog is open.

Audit log tools

The audit log as its screen shows it, and its (undo, or redo).

audit_log

Reads the log without touching the screen, a level of the screen at a time:

Arguments Gives
none The three domains: rips, settings, files, and whether each is empty
domain rips or files Its discs, newest first: disc (the key), name, last (time of the newest change)
domain settings Its events (rows), newest first
domain and disc That disc's events, newest first: its jobs and the rows outside a job
job (alone) The job's own row (event) and its rows in order: sectors, file moves, results
offset, limit The page: limit 1 to 200, default 50; next_offset is the next page's offset, null when there is no more

A domain or disc lists its newest 500 discs or events. Every item says whether it can be undone now: undo.can, undo.redo (the next step is a redo: the item is undone, so audit_redo takes it) and undo.why (why not, the screen's reason in English). A disc of files is undone by its jobs, one at a time, as on the screen.

{"domain":"rips","disc":"SLES_011.35"}:

{"domain":"rips","disc":"SLES_011.35","name":"Alundra","undo":{"can":true,"redo":false,"why":null},
 "events":[
  {"id":41,"kind":"job","job":7,"domain":"rips","disc":"SLES_011.35","what":"repair","before":"Alundra","after":"",
   "source":"mcp","time":"2026-11-08T14:05:00Z","undone":false,"label":"Repair","undo":{"can":true,"redo":false,"why":null}},
  …],
 "offset":0,"limit":50,"next_offset":null,"cut":false}

A row has its id, kind (job, sector, setting, move, delete, result, redump, undo), its job (or null), what, before and after as the log keeps them, file and lba where it has them, source (pad, remote, app, mcp, undo), time, undone and label (what the screen shows, in English). A secret setting's row says only set or cleared, and a secret value is shown as [secret] should one appear anywhere, as in read_log. An old row of the game list's source or filter (saved by earlier versions) says No longer a setting: nothing to undo.

audit_undo, audit_redo

audit_undo undoes one item, as and yes on the audit screen do; audit_redo applies an undone item again, as (Redo) and yes. The audit screen shows on the TV meanwhile.

Argument Meaning
row A row's id: a setting, a sector, a file move, or an undo row
job A job's number: all its kept changes (a repair's sectors, a rename's or a trash move's files)
disc A disc's key in the domain rips: every kept change of it. Only with confirm
confirm Must be true with disc: undoing a whole disc rewrites sectors of its rip and checks it again

Give exactly one of row, job and disc. For a row or a job the call itself is the confirmation the screen asks for. The undo rows are logged as the screen's are (source Undo), and read_log shows audit undo of … by mcp.

{"row":12}:

{"result":{"undone":"row 12","label":"Read speed: Max → 4×","note":"Logged in the audit log as the screen's undo is, with the source undo (read_log notes it was by mcp); audit_log shows the item now."},
 "state":{"screen":"audit_log",…}}

Unlike the screen's , the two never toggle: audit_undo on an undone item is refused (Not undone: row 12 is undone already. audit_redo applies it again.), and so is audit_redo on one that is not undone. Other refusals are the screen's reasons, for example Not undone: Its value was never logged (secret) or Not undone: Read speed has changed since. A job's own row (kind job) is undone by its job.

A big undo (many sectors, or a rip checked again afterwards) answers after about 3 seconds with {"result":{"started":true,"what":"job 7",…},"state":{"screen":"audit_undo",…}} and goes on; get_state shows how far, and audit_log shows the result when it has ended. Every other request is refused at once while it runs.

Game list tools

These work on the game list. A game's id is its source and its place: hdd:/dev_hdd0/PSXISO/Croc 2 (Europe) [SLES_020.88], plus the image's extension for an .iso or .img (hdd:/dev_hdd0/PSXISO/Castlevania - Symphony of the Night (USA).iso), and with /.trash/ for a game in the trash. A game of several discs has the id of its first disc, and each disc has its own id in discs; any of them finds the game. An id stays the same until the game is renamed or moved; the tools that move a game answer with the new ids.

A source is hdd, usbN (the USB drive's port) or all. The filter is the filter panel's, each criterion left out is any:

Criterion Values
format any, 2352, 2048
redump any, verified, unchecked, no_match, not_verified
errors any, with, without
other any, broken, duplicates, sbi_missing
show games (default), trash

For list_games and the actions the source and filter are only for the call. When a game tool needs another source than the list on the screen shows, the screen switches to it, but the source the list returns to (SELECT in the list) is not changed. Only show_games sets the list's own source and filter, as SELECT and the filter panel do. Neither is saved: each start of the app shows the hard disk with no filter.

list_games

The game list, read on its own without touching the screen.

Argument Meaning
source Default all
filter See above
query Only games whose name has this text, any case (at most 95 bytes)
offset The first game to give, default 0
limit 1 to 200, default 50

{"source":"hdd"}:

{"source":"hdd","trash":false,
 "sources":[{"key":"hdd","label":"Hard disk","discs":5},{"key":"all","label":"All","discs":null}],
 "summary":{"text":"4 games · 1 re-rip","games":4,"verified":0,"errors":0,"rerip":1,"broken":0},
 "filter":{"format":"any","redump":"any","errors":"any","other":"any","show":"games"},"query":"",
 "matching":4,"offset":0,"limit":50,"more_than_limit":false,
 "games":[
  {"id":"hdd:/dev_hdd0/PSXISO/Croc 2 (Europe) [SLES_020.88]","name":"Croc 2 (Europe)","disc_id":"SLES_020.88",
   "format":"2352","tracks":1,"status":"unchecked","status_text":"Not checked","errors":0,"redump_name":"",
   "place":{"source":"hdd","label":"Hard disk","folder":"/dev_hdd0/PSXISO"},
   "duplicate":false,"duplicate_of":null,"sbi_missing":false,"in_trash":false,"ndiscs":1},
  {"id":"hdd:/dev_hdd0/PSXISO/Final Fantasy IX (Europe) (Disc 1) [SLES_029.65]","name":"Final Fantasy IX (Europe)",…,
   "ndiscs":2,"discs":[{"id":"hdd:/dev_hdd0/PSXISO/Final Fantasy IX (Europe) (Disc 1) [SLES_029.65]","disc_id":"SLES_029.65",…},
                      {"id":"hdd:/dev_hdd0/PSXISO/Final Fantasy IX (Europe) (Disc 2) [SLES_129.65]","disc_id":"SLES_129.65",…}]},
  …],
 "next_offset":null,"cut":false,"check":{"running":false,"last":null}}
  • status: verified, no_match, unchecked, errors, rerip (a 2048 image) or broken; status_text is the list's own words.
  • matching counts the games the filter and query let through; summary counts the whole source.
  • Paging: next_offset is the offset of the next page, null when there is no more. When a page would be too large for one answer it ends early with "cut":true, and next_offset says where to go on. more_than_limit is true when the source has more than 1000 discs (the list shows the first 1000).
  • check: a running game-list check (running, with its progress) or the last one's result in last: {"running":false,"last":{"discs":1,"checked":0,"failed":0,"not_compared":0,"cancelled":false,"unreachable":true}}.

{"query":"croc","limit":1} → one game, Croc 2; {"limit":2} → two games and "next_offset":2.

get_game

One game as its detail view shows it, with every disc's files and details (the detail view's lines in English).

{"id":"hdd:/dev_hdd0/PSXISO/Croc 2 (Europe) [SLES_020.88]"}:

{"id":"hdd:/dev_hdd0/PSXISO/Croc 2 (Europe) [SLES_020.88]","name":"Croc 2 (Europe)",…,"ndiscs":1,
 "discs":[{"id":"hdd:/dev_hdd0/PSXISO/Croc 2 (Europe) [SLES_020.88]","disc_id":"SLES_020.88",…,
   "files":["Croc 2 (Europe) [SLES_020.88].cue","Croc 2 (Europe) [SLES_020.88].bin"],
   "details":[{"label":"Status","value":"Not checked"},{"label":"Format","value":"2352"},
              {"label":"Disc ID","value":"SLES_020.88"},{"label":"Size","value":"0 MB"},
              {"label":"Place","value":"Hard disk · /dev_hdd0/PSXISO"},{"label":"Cover picture","value":"No"},
              {"label":"Last checked","value":"Never"},{"label":"MD5","value":"Not computed yet"},…]}]}

An unknown id: No game with the id "…" is there now: list_games gives the ids.

show_games

Shows the game list on the screen with a source and filter, set in one step as SELECT and the filter panel set them. They last until the app restarts: neither is saved or in the audit log, and each start shows the hard disk with no filter (the answer's note says so). Use it to show the agent's findings on the TV; list_games reads the games.

Argument Meaning
source hdd, usbN or all. Left out: the source shown now
filter The criteria to set (as above); each one left out keeps its value. "clear" (or {}) clears them, as L1 does. show (games or trash) is the panel's Show: it switches only the view, as in the panel

The answer is what the list shows, its summary line and how many games the filter shows (shown_text, as the list's criteria line has it), and the screen state.

Duplicates on all sources, {"source":"all","filter":{"other":"duplicates"}}:

{"result":{"source":"all","label":"All","trash":false,
  "filter":{"format":"any","redump":"any","errors":"any","other":"duplicates","show":"games"},
  "summary":{"text":"22 games · 1 verified · 2 with errors · 1 re-rip · 1 broken","games":22,"verified":1,"errors":2,"rerip":1,"broken":1},
  "shown":4,"shown_text":"4 of 22",
  "note":"The source and filter last until the app restarts; they are not saved (each start shows the hard disk with no filter)."},
 "state":{"screen":"games","row":0,…}}

Then {"filter":{"redump":"verified"}} shows the verified duplicates (the source and other stay), and {"filter":"clear"} shows every game again (the view, games or trash, stays as it is).

Refusals: a source that is not connected (USB 5 is not connected; list_games gives the sources., nothing is changed), an unknown criterion or value (a parameter error that lists the allowed ones), while the game list checks, and while a dialog is open. The list cannot filter by name, so show_games has no query.

show_game

Shows a game's detail view on the screen, as on its row in the game list does. {"id":…}; when the game is on another source than the list shows, the list goes there first, as for the other game tools (the source chosen with SELECT or show_games stays). To read the details, use get_game.

{"result":{"shown":"Alundra (Europe)","discs":1,"note":"The game's detail view is on the screen: get_game reads it."},
 "state":{"screen":"game_detail",…}}

Refused when the list's filter hides the game (The list's filter hides this game: show_games with filter "clear" shows every game.), while the game list checks, and while a dialog is open.

check_game, check_games, cancel_check

check_game {"id":…} checks one game against Redump from its files, as in the list does. check_games {"source":…, "filter":…} (both optional; source default all) checks every game that has no Redump result yet and can be checked, as START does. Both need Redump on (online or a local copy), answer at once, and the check runs on:

{"result":{"started":true,"discs":1},"state":{"screen":"games",…}}

While it runs, get_state shows the screen game_check; when it has ended, list_games shows the statuses and check.last. cancel_check (no arguments) stops it as does: the game being checked and those not reached keep the result they had. While a check runs, the other game tools are refused; the reading tools still work.

Refusals, for example: Redump is off: turn it on in the settings (online or a local copy) to check games, and No game list check runs: there is nothing to cancel.

rename_game

Renames every file of the game (all its discs, the .cue's FILE lines too), as START in the rename view does. Logged in the audit log and undoable there.

Argument Meaning
id The game
pattern %T title, %N Redump's name, %D disc ID, %R region, other characters as they are (at most 95 bytes). Empty: %T [%D]

{"id":"hdd:/dev_hdd0/PSXISO/Alundra (Europe) [SLES_011.35]","pattern":"%T"}:

{"result":{"renamed":"Alundra (Europe)","note":"Renamed: Alundra",
           "discs":[{"id":"hdd:/dev_hdd0/PSXISO/Alundra","name":"Alundra"}]},
 "state":{"screen":"games",…}}

Refused when the name stays the same (The name stays the same), a game with the new name is there, a drive or a check uses the game, the new name starts with a dot, or the pattern has a control character.

trash_game, restore_game

trash_game {"id":…} moves the game, every file of it, to .trash in its PSXISO folder, as does. restore_game {"id":…} (an id with /.trash/) moves it back. Both are logged in the audit log and undoable there, and answer with the new ids:

{"result":{"trashed":"Final Fantasy IX (Europe)","note":"Moved to the trash: Final Fantasy IX (Europe)",
  "discs":[{"id":"hdd:/dev_hdd0/PSXISO/.trash/Final Fantasy IX (Europe) (Disc 1) [SLES_029.65]","name":"Final Fantasy IX (Europe) (Disc 1) [SLES_029.65]"},
           {"id":"hdd:/dev_hdd0/PSXISO/.trash/Final Fantasy IX (Europe) (Disc 2) [SLES_129.65]","name":"Final Fantasy IX (Europe) (Disc 2) [SLES_129.65]"}]},
 "state":{…}}

restore_game on a game that is not in the trash: The game is not in the trash: there is nothing to restore.

empty_trash

Deletes every game in the trash of one source, as START and yes in the trash do. This is final: the files cannot be restored, not even from the audit log.

Argument Meaning
source Required: hdd or usbN. all is refused: a trash is emptied one source at a time
confirm Must be true; without it nothing is deleted
expect Optional: what you saw in that trash, either the number of games, or the id of every disc in it (a game's id and, for a game of several discs, each id in its discs), in any order

List the trash first (list_games with the source and "filter":{"show":"trash"}) and pass what you saw as expect: if the trash holds anything else by then, nothing is deleted.

{"source":"hdd","confirm":true,"expect":["hdd:/dev_hdd0/PSXISO/.trash/Final Fantasy IX (Europe) (Disc 2) [SLES_129.65]","hdd:/dev_hdd0/PSXISO/.trash/Final Fantasy IX (Europe) (Disc 1) [SLES_029.65]"]}:

{"result":{"emptied":true,"note":"The trash was emptied: 4 files deleted"},"state":{…}}

Refusals:

  • Without confirm: Not emptied: emptying the trash deletes its games' files for good (they cannot be restored, not even from the audit log). Call empty_trash again with confirm: true to empty it.
  • expect does not match: Not emptied: the trash of Hard disk has 1 game now, not the 2 expected. Nothing was deleted: list_games (this source, filter show: trash) shows what is there. (With ids: … has 2 discs now, not the 1 expected.)
  • An empty trash: The trash is empty: there is nothing to delete.

Screen names

get_state names the screen:

screen The screen shown
home The drives' tabs
settings The settings
drive_settings A drive's own settings (SELECT on its tab); drive says which
filename The file name pattern
webhooks, webhook_headers The webhook settings and their headers
about About PSX RiPPER
audit_log The audit log
audit_undo An undo of the audit log runs (its wait screen); audit_undo in the state says how far
remote_control The Remote control menu
redump_download Downloading the local Redump copy
games The game list
game_filter The game list's filter panel
game_detail A game's details
game_rename The rename view
game_check The progress of a game-list check
dialog A dialog of the app's is open; dialog.over names the screen under it

go_screen opens the screens in its list; the others are reached from them (with press_button), by the tools (show_game, redump_download, audit_undo) or by the app itself.

Rules

  • Actions first, buttons as a fallback. Everything the app does has a tool: the drive actions, set_setting and reset_settings, redump_download, webhook_test, preview_name, get_app_info, audit_log, audit_undo and audit_redo, and the game tools. They do the work directly, with the pad's checks. press_button is for answering a dialog ( yes or OK, no) and for moving around a screen; it never presses into the console's on-screen keyboard.
  • on the home screen is refused: it would exit the app, and MCP with it.
  • A call is the confirmation. Where the pad asks Cancel the rip? or Undo …?, the cancel and audit_undo/audit_redo calls themselves are the yes, and the log says so. What is final or large asks for confirm: true: empty_trash, reset_settings without a drive, and an undo or redo of a whole disc.
  • No dialog left behind. A Redump download started with redump_download ends without the dialog the pad gets; get_app_info has its result.
  • rip replaces an existing rip of the disc at the destination without asking, like .
  • Dialogs and the keyboard. While the app's own dialog is open, the actions, the game tools, reset_settings, redump_download, audit_undo, audit_redo, webhook_test, go_screen and go_tab are refused with the dialog's text and the buttons that answer it: answer it with press_button first. While the console's on-screen keyboard is open, these and press_button are refused: it is answered on the console. The reading tools and set_setting work all the time.
  • Busy after 5 s. An action waits at most 5 seconds for the app to take it and finish it. If the app is in the middle of something long, or the console's own menu is open, the answer is Busy: … with the screen it is on; a request the app took but did not finish in time answers The app took the request but did not finish it within 5 s. Look at get_state and try again.
  • One request at a time. Two agents (or two calls) are served one after the other.
  • The game list's source and filter are not saved. A game tool may switch the list on the screen to another source, but the source the list returns to stays the one chosen with SELECT or show_games (which sets the source and filter as SELECT and the filter panel do). Both last until the app restarts; each start shows the hard disk with no filter.

Security

  • Your home network only. MCP has no password and no login: anyone on the network who can reach the console can use it while it is on. Never forward port 8090 to the internet. Turn MCP off when you do not use it.
  • Web pages cannot use it. A request from a web browser carries an Origin; the app answers only requests with no Origin (programs such as MCP clients) or from its own pages, exactly http://<the console's address>:8090 (not a host name for it, not localhost). Anything else gets Origin not allowed, so a web page you visit cannot use your browser to control the console. The remote view's pad has the same check: open its page by the console's address.
  • Secrets are never shown. The webhook address and the headers can be set over MCP but never read: list_settings and set_setting say only whether they are set, read_log and audit_log show [secret] in their place, the audit log notes only set or cleared, and webhook_test answers with the status alone.
  • Nothing else is reachable. The app's debug commands are not part of MCP.
  • Connection limits, so a slow or stuck client cannot hold the app: at most 4 requests at once (one more gets 503, try again), a request must arrive whole within 10 seconds and be at most 16 KB, a client that has not taken an answer within 10 seconds is dropped, and an answer is at most 512 KB. The web server runs at a low priority: a rip does not slow down because MCP is on.

Limits

  • list_games has no time limit. The first call reads the source's folders (and with all, every source), which on a large USB drive can take a while; the scan is kept, so the next call, and a retry after a client gave up waiting, is fast. A new scan is made when the app changed files, when the hard disk's folder changed, or after a minute for a USB drive.
  • A game action on another source than the one on the screen first reads that source; on a large USB drive this can take longer than the 5 seconds and answer Busy. The screen stays on the new source, so trying again works. So does show_games: its source and filter are set before the read, and stay set.
  • A page of list_games with many large games can be cut (cut); a smaller limit helps. If not even one game fits: The answer is too large: use a smaller limit.
  • A game whose file name is not valid UTF-8 can be listed but not acted on by id: its id shows ? for the invalid bytes and matches no file, so get_game, check_game, rename_game, trash_game and restore_game answer that no such game is there, and empty_trash's expect with ids never matches it. Use the console for it, and empty such a trash with a count.
  • get_screenshot does not show the console's own dialogs or keyboard.
  • audit_log lists a domain's or disc's newest 500 discs or events (the screen shows 128).
  • webhook_test waits at most 4 seconds. The console's network has no time limit of its own for looking up a host name or connecting, so a test to an address that never answers keeps running in the background until the console gives up; another test is refused until then.

docs/mcp.md on GitHub (opens in a new tab)