Skip to the text
The Colophon Project

The MCP server

An agent has no eyes on the glass and no fingers on the keys, and what it learns of a machine is what it asks for. This server is where it asks. It speaks the Model Context Protocol over its standard input and output, to one client at a time, and holds as many machines as that client stands up, each behind a handle of its own.

Starting it

Each release carries the server for each of six platforms with its firmware in roms beside it, as the npm package colophon-mcp does; make builds it from its source as target/release/colophon-mcp. The server is started with the directory its firmware is read from, which the package gives it of itself; in an archive’s folder it is started as

./colophon-mcp --roms roms

and in a checkout beside the emulator as target/release/colophon-mcp --roms ../colophon-emulator/roms.

The directory holds the firmware as a release carries it and as the emulator’s make roms fetches it: cpc6128.rom, cpc664.rom and cpc464.rom, each the operating system followed by BASIC; amsdos.rom, for the machines with a disc interface; and spectrum48.rom. A file is read when a machine that needs it is opened, so a missing one is reported then, by name, and nothing else is refused for it. A file must be the size of the firmware it is named for, where the emulator’s command line takes the first bytes of a longer one.

A client is given npx -y colophon-mcp, or this command line with absolute paths, which it starts and speaks to; installing it gives the line for each client.

What it answers

A request that names its revision in its _meta, as the 2026-07-28 revision has every request do, is answered in the revision it names, server/discover among them. A client that opens with initialize instead is answered in 2025-11-25 or 2025-06-18, as it asks, and in 2025-11-25 if it asks for any other. A request in a revision the server does not speak is refused with the ones it does.

machine_open builds a machine by the name the emulator’s command line gives it, and wires it as the command line does: the same RAM, the same firmware in the same sockets, and the disc interface where the machine has one. A CPC’s 6845 is of the type it is given, numbered as the Compendium numbers the five, and of type 0, the first CPCs’ type, where it is given none; set does not change it. Given a snapshot, it loads it as the command line does, into the machine named, once the firmware is fitted; given a state file, it stands the machine up as the state kept it. machine_list names the machines standing, and machine_close puts one away once what it was asked before is done. run and step move a machine, and call runs a routine of its own; inspect and set read and write what it holds, peek and poke its memory, find searches it, and compare sets a CPC’s RAM beside a state kept from it or beside another CPC’s. monitor shows its picture, screen reads a CPC’s memory as one, and disassemble reads its memory as instructions. type types at it and run holds its keys down, disc_insert and tape_insert give it a disc or a tape, disc_save writes one of its discs out as an image, and snapshot_save writes it to a file. state_keep keeps it as it stands, state_restore puts it back there, state_save writes a state to a file and state_drop lets a state go, and journal lists what has been done to it. Each tool’s description is the reference for it; an agent reads them from the server. Beside them the server sends its instructions, which a client may set before the model as a session opens: what the tools are for, and what holds across them.

An address or a port is answered in hexadecimal after &, &BB06, and so is a byte a run gathers, &3E, as a CPC’s own manuals write them and as disassemble writes the numbers in its instructions; an address past &FFFF, in a 6128’s second 64K, takes five digits. A register of a CPC’s sound chip, its PSG, is a number, 7, as SOFT 968, Amstrad’s firmware manual, numbers them, and so is a register of its 6845, 12, as the chip’s datasheet numbers R12. The bytes peek, compare and disassemble answer are pairs of digits, as poke and find take them. A request may give an address in the forms its schema lists, and where it reads memory, in peek, disassemble and a capture, cpu.sp or cpu.pc for the address the register holds; and the journal writes every address and port a request gave, and every byte a condition’s value and value_to give, after &, whatever form it came in, when’s as pairs of capital digits, or ??, a space between each, and a range of one address, register or byte as that one, leaving out a test of every byte and a changes that is false; a capture’s cpu.sp and cpu.pc, and the values set and call write, it keeps as they were given. inspect and set answer the emulator’s struct as it holds it, in numbers, and set takes hexadecimal for a field of unsigned integers too. An argument of the wrong kind is refused with its name, and a request missing one with every argument the tool requires, where it requires more than one. No schema names a format, since every format JSON Schema defines is of strings and a client need not know any other.

Time

Nothing but run, step, call and type moves a machine, and each answers with the ticks it took, the frames the monitor retraced in them, and the machine’s tick since it was stood up: from power-on, or from its snapshot. state_restore puts a machine back at a tick it stood at before, and a machine stood up from a state file begins at the state’s. The same requests of the same machine give the same answers.

A run goes by frames or by ticks. A frame is the monitor’s and ends as its retrace sends the beam to the top-left corner, so a run by frames leaves a whole picture on the monitor; a CPC’s frame is whatever its 6845 is programmed to make and may never come, so such a run is held to twice a standard frame’s ticks for each frame asked. A run by ticks runs that many, which is how the emulator’s command line counts its frames: 79872 ticks to a CPC’s, 69888 to a Spectrum’s. A step runs instructions, each to its end and through the response to an interrupt taken there, so a step can land in a handler. A run or a step ends between instructions, a few ticks past what was asked if the instruction standing there runs on, unless it ends inside a chain of prefixes, which is no instruction until it is done; the answer then says it stopped unsettled. A run stops sooner at the end of a HALT that leaves interrupts disabled, and says it stopped halted: the processor waits there for good, since IFF1 shuts out the interrupt and neither machine raises the non-maskable one, so nothing the machine does of itself carries it further. A run that begins there runs as asked, so the frames a whole picture wants still pass, unless set has written in a non-maskable interrupt, which wakes it, or written the processor as no longer halted, which carries it past the HALT. Typing takes its ticks all the same.

One request runs at most 1500 frames, or the ticks of 1500 standard frames, which is half a minute of either machine’s time, or a minute of a CPC’s whose frames do not come; or 100000 instructions, under a second. On the machine the server was written on, 1500 of a CPC’s frames run in about six seconds, well inside the minute a client of the protocol’s TypeScript SDK waits for an answer.

Stopping and watching

A run can be told where to stop and what to watch for on the way, and a condition says either in the same words: an instruction about to begin at an address or in a range, a byte read or written there, a port read or written, or a register of a CPC’s PSG or its 6845 written. A port is matched on the address lines its device decodes, which the condition names, because a CPC’s chips each answer a few lines of the address and not one number. An instruction or a read may also be asked to come from the RAM, or from a ROM: a CPC’s Gate Array enables its lower ROM, the firmware, over &0000 to &3FFF, and an upper ROM, chosen by its number, over &C000 to &FFFF, and while a ROM is enabled the processor reads it, and its writes go to the RAM beneath, so that &0038 is the firmware’s while the lower ROM is enabled and the RAM’s while it is not. A Spectrum’s ROM is below &4000 for good. A CPC’s processor reaches its PSG, the AY-3-8912 that is its sound chip, through the PPI: port A carries the PSG’s data and the top two bits of port C its function, and while those bits give WRITE the PSG takes port A’s byte into the register it has selected. The byte the PSG is given as its register address names the register in its low four bits; its high four are a chip select, which General Instrument’s data manual has the chip match against 0000, so that a byte above 15 selects no register. In deathsoft and lvd’s model of the die such a byte also leaves the chip unselected, and so does a reset; the emulator follows it, resetting the chip at power-on, so that an unselected chip takes no write, and a psg condition matches none, until a byte from 0 to 15 is given as the address, as the tests find. The emulator runs the PSG at each access to the PPI, by port C as the access leaves it, so each access that leaves port C giving WRITE to a selected chip is a write and a match: an IN from the PPI and the PPI’s bit set/reset among them, often the same byte to the same register again. A psg condition names registers by their numbers, 0 to 15, and its matches give the byte port A gave the PSG, of which a register narrower than eight bits keeps only its low bits. The tests write registers through the routine cpctech gives for it and through the firmware’s MC SOUND REGISTER and SOUND QUEUE, and find the writes where a listing of the 6128’s firmware makes them, in its order. A run given conditions in until stops at the first match: before the instruction, so that the machine stands at it unrun, or at the end of the instruction that made the access, so that it stands between instructions as every run leaves it. A run that begins on an instruction it was stopped before runs that instruction first. An instruction that repeats, LDIR and its kin, begins again at its own address each time it repeats, since the program counter is decremented by two and the instruction is repeated, and is matched each time: an LDIR that copies five bytes is five matches, as the tests find. Conditions in gather are listed rather than stopped at, each match with the tick it came at, the address, the port or the register and the byte, where the instruction that made it begins, and where the monitor’s beam then stood; a port read gives the byte the processor took, which a CPC’s board can change while it holds the read. On a CPC a match also says which memory held its instruction or its byte, where the address alone does not: rom names the lower ROM, or an upper ROM by its number, as WinAPE’s debugger names an upper ROM’s block, and ram the RAM’s own address, where a 6128’s RAM configuration pages it in elsewhere, as peek’s ram counts it; pc_rom and pc_ram say the same of the instruction that made an access. A number selected with no ROM fitted is answered by ROM 0, as Kevin Thacker’s cpctech says BASIC is, and the tests find the 6128’s firmware walking its ROMs as its listing walks them, from ROM 15 down to 1, reading ROM 0 at each number but 7, where AMSDOS answers, and starting BASIC and then AMSDOS at &C006 of each; they find the firmware’s interrupt entry at &0038 run from the lower ROM and from the copy the firmware makes of it in RAM, and a condition on the RAM’s stopping only at the copy. A list fills at 128 matches, keeps the rest of the instruction that made the 128th and of the response to an interrupt taken at its end, and stops the run where every run stops, so that the next run gathers on from there and nothing is lost between them. The matches a run gathers are those a run stopped at each of them in turn would stop at, and the tests hold the two to each other. Given gather_last, a run keeps the last 128 matches instead, and runs on past them as though nothing filled, so that one stopped by until, or at its end, lists what led there; the tests hold it to the tail of a gathering carried through many runs, and find the last instructions of a routine in the 48K ROM where its disassembly ends it.

A CPC’s 6845, its CRTC, answers an I/O request with A14 low, its registers selected through &BCxx and written through &BDxx, and since it takes its direction from the port’s address, and not from the processor’s RD and WR, an IN from either port selects or writes too: an IN A,(n) gives it A, the top half of the port’s address, and the emulator gives it the top half of the port’s address for every IN. A crtc condition matches each write to a register, by the number the chip’s address register holds, 0 to 31, of which R0 to R17 are the chip’s and 18 to 31 none, and gives the byte the chip was given, of which the register keeps as many bits as it has. It matches where the chip takes the write, which on types 3 and 4 is an OUT (C),r’s 4th µs rather than its 3rd, while an OUT (n),A reaches every type in its 3rd µs and an OUTI in its 5th. A register selected is no write, and in and out on &BC00 with the mask &4300 find it. The tests write registers with OUT and with IN A,(n) and select one with IN A,(n), and find the firmware loading its table as it starts and setting the screen’s start through MC SCREEN OFFSET where its listing does.

A condition may also name the bytes it matches. value, with value_to for a range, matches an access only where its byte lies between them: the byte read or written, a port’s written, or a register’s given, so that one condition on the Gate Array’s port picks out the writes that set its mode, its ROMs and its interrupt counter, &80 to &BF, from those that choose its pens and inks, each command named by the byte’s top two bits, bit 5 having no effect. changes matches a write, to memory or to a register of the PSG or the 6845, only where it leaves the byte holding other than it held, a register as it keeps it, and with value on memory it is the write that makes the byte that: the moment a game’s counter becomes 6, rather than every frame it is written 6 again; a register’s value is the byte given it, of which it may keep only some bits. A write beneath a CPC’s enabled ROM is held to the RAM it lands in, and a write to a Spectrum’s ROM changes nothing. A port read’s byte is not tested, on either machine, since a CPC holds the read over several ticks and the processor takes the byte the last of them leaves, where a condition is decided on the first.

A condition may also ask what memory holds as it matches. when names an address and the bytes that must stand there, written as find takes them, ?? for a byte that may be anything, eight at most, so that a routine run while a game is in one state, or the line a BASIC program has come to, is one stop rather than many, with a reading after each. The bytes are read as the processor reads them, through a CPC’s banking and its ROMs, at the moment the match is made: for an instruction, before it begins; for an access, on the tick it is made, so that a write’s own byte has landed and the bytes the same instruction writes after it have not. A stop or a capture on an access is made at the end of its instruction, so either may find bytes other than those when read. A word is written a byte at a time, and is asked for on the write of the byte that goes last, with when on both: LD (nn),HL loads L to the address and H to the one after, the first before the second, as the emulator writes them, so a word it writes is asked for on its high byte, and a condition there matches every such write, the same value written again included, and no write of the low byte alone, while a PUSH writes the high byte first and is asked for on its low byte. changes on that byte tests that byte alone, and passes over a word whose last byte already stood. A condition on both bytes matches the write that completes the value, whichever byte goes last, but also the first of the two writes wherever the byte it writes, beside the other as it still stands, reads as the value asked for, whatever value the word is being given, so that both writes of one word may match. The tests hold a stop on when to the first of the stops a client makes without it, reading memory after each, at which the bytes stand, a gathering on when to the stops it makes one run at a time, and a count to their number. They find when read before an instruction runs, and after a write’s own byte has landed but before the next its instruction writes, which a stop there then finds; a condition on the high byte matching once, where a program counting a word up writes the value; one on both bytes matching there, again where a later value’s low byte meets the high byte still standing, and at both writes of one word where its high byte already stood; and a CPC’s when reading its lower ROM where the ROM is enabled.

A condition in until or gather may also capture the machine at each of its matches, so that what would be read after a stop comes with the match: values of the processor, each by the path inspect reads it at under cpu, and spans of memory, each read as peek reads it, from an address or from where SP or PC points as the capture is taken, so that a match carries the top of the stack, a return address among it, or the bytes of the instruction it stands at. An instruction about to begin captures the machine it finds, as a stop before it leaves the machine. An access captures the machine as the instruction that made it leaves it at its end, where a stop on the access stands, and every access of one instruction alike, so that both writes of a PUSH capture the stack pointer it leaves; inside an instruction the processor’s registers stand in the emulator’s own order of working, which no documentation grades. An interrupt taken at the end of the instruction is accepted before the capture and answered after it, and the stop runs on through the response, so there the two differ: the capture has the instruction’s program counter and stack, but IFF1 clear, as the processor clears it as it takes the interrupt, and IFF2 with it if the interrupt is INT, and the emulator’s own machinery for the processor standing as the response begins. An access the response itself makes captures the machine at the response’s end. A match answers what it captured in its captured, and a match of several conditions captures what each of them names. A gathering any of whose conditions captures fills at 32 matches, or with gather_last keeps the last 32, since each carries more; with a path it fills at 1048576 as any gathering does, each line with its capture. The tests hold every capture across a stretch to what a stop at each of its matches reads back with inspect and peek, and to what a twin gathering over the same stretch captures; they find the capture before the response to INT and to an NMI written in with set, and TXT OUTPUT, at the address the 6128’s firmware listing gives, handed each character BASIC’s PRINT prints, one call at a time, in A.

Conditions in count are counted rather than listed: each place that matches, an instruction begun at an address, a byte read or written there, a port by the whole of its address, or a register of the PSG or the 6845, is answered once, with how many times it matched, executes first, then reads, writes, ports, and the PSG’s registers and the 6845’s, each kind by address or register, or by pc. A place in memory is a byte of it, so that on a CPC the lower ROM’s &0038 and the RAM’s are two places, as are a 6128’s two banks of RAM at one address, and a byte the processor reaches at two addresses, as a 6128’s RAM configurations can page one, is one place, told by the address of its first match. Each is told by its first match, with its tick and where the instruction that made it begins, and an instruction with the bytes the processor fetched as it first began there, so that code a program writes, runs and wipes again still says what ran. A count asked to run over &4000 to &BFFF says which of a program’s instructions ran, how often and when each first did, and one over a single address how many times the instruction there began. A count by pc counts an access by where the instruction that made it begins rather than by its address, so that a count of the writes to a screen says which instructions wrote it and how many writes each made; the response to an interrupt is no instruction, so what it does, the return address it pushes and the vector it reads in mode 2, is then not counted. The tests count the 48K ROM clearing its screen so, and find the four instructions its disassembly names, each first writing where the disassembly says. Counted by address, a port is the whole address on the bus, whatever bits the condition’s mask decodes, so a port whose address carries the value written, as OUT (n),A puts A on the high byte, is counted at as many places as there are values. It fills at 256 places, keeps the rest of the instruction that filled it, and stops the run where a full gathering does; the next run counts afresh from there. A count is a gathering tallied by place, by address or by pc, and each place is told by the first match gathered there since the run began; the tests hold the two to each other.

An answer is bounded to what a model can take in, and a question about every frame of a stretch is bigger than that. Given a path, a run or a call writes what gather and count find to a new file instead, a line of JSON for each match and each place or range, as the answer would list it, after the list it belongs to, the matches first, for a client to read with tools of its own. The answer names the file by its path and its SHA-256 and says how many lines each list holds. Written so, a count never fills, so a run by frames counts all its frames, a map of every byte a program reads and writes over them; and a gathering fills at 1048576 matches, some 100MB, or some 750MB where each captures all a request may name, where an answer’s fills at 128, which holds a trace of every instruction a program runs over a hundred frames or so, and its writes to the PSG over many more. The file is made before the run begins, only where no file is, and a request whose file cannot be made is refused with the machine as it stood. It stands empty at its path through the run, and is written once the run is over, before the answer, so a file holds what it is to hold once an answer names it by its hash. One that cannot be written whole then is taken away again, and the request, which is not taken back, is refused with how far it ran and where the machine stands, and journaled without the file. The tests hold a file to what runs that fill and go on list over the same stretch, and to what a twin’s answer lists.

A count by range is a count by address told shortly, for a question about which stretches of memory a program runs, reads or writes rather than about each byte. Places of one kind whose bytes follow one another with no gap between them make one range, both at the addresses the processor first reached them at and in the RAM or the ROM that holds them, so that no range runs on from the lower ROM’s &3FFF into the RAM’s &4000, nor from the block a 6128 pages in at &4000 into the first 64K’s at &8000, nor from one of its blocks into the next where the processor reads them apart. An instruction covers the bytes it fetched before the first that does not follow the one before it in the same RAM or ROM, as a block a 6128 pages in at the next 16K may not, so code laid end to end is one range, an instruction run from inside another’s operand belongs to that one’s range, and an instruction jumped over ends it. An instruction longer than four bytes, which only a chain of prefixes makes, is known by its first four alone, so the one after it begins a range of its own. A byte read or written is one byte, and ports and registers follow one another by number. Each range is told by its first place and its last, how many places it holds and how many times they matched together, and not by when each first matched or the bytes it ran with, which a count by address tells. A count by range never fills, since its places are folded only once the run is over: its answer lists the first 256 ranges and says how many there were, and a file takes them all. The tests fold what a twin counts by address over the same stretch into the ranges; hold a short program’s ranges to the lengths of its instructions, its ports and the PSG’s and the 6845’s registers to their numbers; keep ranges apart where either machine’s ROM ends at &3FFF, between a 6128’s blocks, and where an instruction runs on into a block paged in; and find the 48K ROM’s routine that clears the screen as the two stretches of code its disassembly lays out, with the one instruction between them that the routine jumps over, and the screen it clears as one range.

A read is the program reading memory as data. The bytes of an instruction are never one: its opcodes are told by the processor’s M1, and its operands by the emulator, which knows them where the bus does not. The return address an interrupt’s response pushes names no instruction, because none made it.

call runs a routine the machine holds as its own CALL would: the program counter is pushed where the stack pointer stands, and the call ends when the processor comes back to it with the stack pointer where it was, which is where the routine’s own return leaves it. A machine called into thus stands where it stood, with whatever the routine left in its registers and memory, and a processor halted on HALT is halted again once the routine has returned within the call. The values given with a call are written first, as set writes them. A routine that halts on HALT with interrupts disabled stops the call there, as it stops a run, and the call says so at once rather than waiting out its frames: a routine that waits on HALT for the next frame, called with IFF1 clear, as inside an interrupt’s handler or after DI, waits for good. A call is refused, and the machine left as it was, where the address pushed would not read back, under a CPC’s enabled ROMs or in a Spectrum’s, and inside a chain of prefixes, which is no place between instructions. The routine is read where the processor reads its address, so beneath a CPC’s enabled ROMs a call runs the ROM and not the RAM there, as the tests find with the ROMs enabled and with them paged off. Interrupts stay as they stand, so their handlers run within a call only if IFF1 is set: a run by frames ends just past the retrace, which on either machine’s firmware comes inside the handler of the frame’s interrupt, with IFF1 clear, and a routine called there runs with no interrupt at all unless it enables them itself, as the tests find on both.

What a machine holds

inspect reads the emulator’s own struct for the machine, cpc_t or spectrum_t, by the path a C expression gives a field of it: cpu.pc, crtc.registers[12], fdc.units[0].head. Its fields keep their headers’ names, and a C enum’s value is its enumerator’s name; pointers are not followed, and peek reads memory instead. The fields a program works with most, the chips’ registers and counters, are named part by part in the tool’s description; most others are the chips’ inner workings, the board’s links and drives, or the emulator’s machinery for stepping the chips a tick at a time, which no silicon holds as such.

set writes by the same paths, every value given or none. The core indexes and sizes by some of its fields without checking them, because nothing it does takes them out of range: the size of the RAM, the RAM configuration of a 64K machine, the pen that picks one of the Gate Array’s seventeen inks, the PSG’s selected register, the disc controller’s counts of the bytes it has taken and given and the drive its command chose, the monitor’s size, and the processor’s count of the cycles under way. A value past those is refused, with the range that would do, and so is a WAIT written on a Spectrum’s bus, which nothing there drives and which would hold its processor for good, and any other type for a CPC’s 6845, in crtc.type or in the copy a line-end room keeps to take back; the machine stands as it was. A CPC’s memory map is worked out again after every write, and a Spectrum’s ULA stood at the tick of the frame its frame_tick names, from which its beam is reckoned.

peek and poke reach memory as the processor would: through a CPC’s banking and its ROMs, so that a write under a ROM lands in the RAM beneath it, and into a Spectrum’s ROM not at all. Both also reach a CPC’s RAM itself, by the RAM’s own addresses as a base given to screen counts them, whatever is paged in: the RAM beneath the ROMs, and a 6128’s second 64K at &10000 to &1FFFF. A run stops under whatever banking the program left, and a 6128 game that keeps its sprites in the second 64K may leave a block of it over its own variables, so a CPC’s answer to an address names the memory its first byte was in, as a match names it: the ROM, or the RAM by its own address where that is not the address given. Past the next 16K boundary a span may reach another memory, &0000 among them where one read from where SP or PC points wraps, and the answer does not name it. peek from an address given reads nothing past &FFFF; from where SP or PC points it runs on past it to &0000, as the processor’s own addresses do, and so does a capture’s span, since where a register points is not known until the read is made.

find searches memory as peek reads it, through the banking and the ROMs or, given ram, in a CPC’s RAM by the RAM’s own addresses, for bytes or for text, and answers the addresses where they lie, lowest first, 256 at most, with how many there are in all and where to search again for the rest. Its bytes take ?? for any byte, and since an address follows its opcode low byte first, the two bytes of an address find every CALL, JP and LD that names it, and every table that holds it. Its text matches each character on the low seven bits of a byte, because both machines’ ROMs end many of their words by setting the eighth bit of the last byte: the 48K ROM its keywords and its messages, as The Complete Spectrum ROM Disassembly writes them, and the 6128’s firmware the words of its cassette manager’s messages, as its listing writes them. The tests find three routines of the 48K ROM wherever the disassembly names them, and nowhere else, and a keyword and a message of the 48K ROM and the first word of the cassette manager’s messages where the listings put them; and they find bytes beneath a 6128’s lower ROM and at the top of its second 64K, by the RAM’s addresses, where its banking put them, and with the ROMs disabled the same bytes in its base 64K by either way of asking.

compare sets a CPC’s RAM beside a state kept from it, or beside another CPC’s RAM as that machine stands, byte by byte and by the RAM’s own addresses, whatever either has paged in, and answers where they differ: each run of neighbouring bytes that differ, lowest first, with the bytes on both sides, 256 runs and 2048 bytes at most, with how many bytes differ in all and where to compare again for the rest. A state kept before a run or a call, compared after it, gives the bytes the run or the call changed, and so narrows the search for a program’s variables to those; a byte written with the value it held changes nothing, and is not listed. Two RAMs of different sizes are compared as far as the smaller reaches. The tests find the bytes the 6128’s firmware writes for a character just where SOFT 968 lays its matrix out on the screen, and nowhere else on the screen, both against a state kept before and against a second machine made alike; and they find a 6128’s RAM compared alike under RAM configurations 0 and 4, beneath the lower ROM and on from the base 64K into the second.

The picture

monitor answers with the picture on the machine’s monitor as a PNG, beside the JSON that says which machine, at which tick, how large, and where the monitor’s beam stands. The picture is cut and drawn as the emulator’s command line cuts and draws its screenshots, so that one written there and one shown here can be laid over each other and compared pixel for pixel, which the tests do: a CPC’s 768 by 544, a sample of the raster to each pixel across and each line drawn twice, and a Spectrum’s 352 by 264. Each pixel’s index in the PNG’s palette is what the machine put on the cable, a CPC’s hardware colour code or a Spectrum’s four bits of brightness and colour, and the palette is the colour each stands for, so an agent that reads the file gets the machine’s numbers back. The beam paints the picture as the machine runs, so after a run by frames that ran all its frames it is whole, and after any other run, a step, a call or typing the lines above the beam are this frame’s and those below it the frame’s before; the answer’s whole says whether the beam is partway through the picture. A frame may also never end. A CPC’s 6845 gives a line sync only where its character counter meets R2, so one set with R0 below R2 gives none once its counter has come round, and the Gate Array, which begins and ends its frame sync by counting the 6845’s line syncs, never begins one, or never ends the one it was giving: a run by frames ends at its limit having run none, and the picture keeps what the frames before it painted, however long ago, as the tests find of a frame sync that never ends. A real monitor’s oscillators would run on without sync and roll the picture, which the emulator’s monitor does not model. A line that a frame shorter than the picture never reaches keeps what an earlier pass left there.

screen reads a CPC’s memory as a picture, as the player’s screen panel reads it and with the same names for what it is given: video, the way the CPC’s video hardware reads the screen; linear, a flat buffer; or columns, one that runs down before across, so that a sprite sheet, a font or a buffer built off the screen shows its shape when read in the layout it was written in. Whatever is not given is read from the machine as it stands: the start, the width and the height from the 6845, and the mode in force and the sixteen inks from the Gate Array, so that a screen asked for nothing draws the memory the 6845 is displaying, at the scale monitor draws it, and the tests hold the two to each other in each of the four modes, and with the 6845 counting rows of five lines and of sixteen, starting partway into a 2K, and carrying a screen of 32K from one 16K into the next. It reads the RAM as the video hardware does, whatever ROM the processor sees over it: the video reading the base 64K, and the others a 6128’s second 64K as well, at &10000 to &1FFFF. Each pixel’s index is its pen rather than a colour, so the pen a sprite’s mask keeps clear stays one number in the file whatever ink it is given.

Reading memory as instructions

disassemble reads memory as the instructions the processor would run from an address on, or from where PC or SP points, each with its address, its bytes and what they say, through the banking and the ROMs as peek reads it. Given ram, it reads a CPC’s RAM by its own addresses instead, a 64K of it at a time, the base 64K or a 6128’s second, and lists it as it lists that 64K when the processor reads it alone, with the ROMs disabled and the RAM configuration 0, or 2 for the second: the same bytes and the same texts, a relative jump’s target and a firmware name among them, wrapping within the 64K, each instruction named by its address in the RAM, so that what lies beneath the ROMs, or in a bank not paged in, is listed without paging anything. The tests hold a listing of either 64K to the processor’s own of it so paged. It writes Zilog’s mnemonics, numbers in hexadecimal after &, and the target of a relative jump worked out, and names the undocumented instructions as the player’s disassembly names them, ED FF among them as WinAPE’s BRK. An instruction is what step runs, so a chain of DD and FD prefixes is one instruction, ruled by its last prefix. On a CPC, the two bytes after a firmware restart that takes an address, LOW JUMP (RST &08), SIDE CALL (RST &10), FAR CALL (RST &18) or FIRM JUMP (RST &28), are listed with it as the word they hold, DEFW &93FE, as SOFT 968 writes a FAR CALL’s, and the listing reads on after the word, as it reads on after the JP or the CALL SOFT 968 likens each restart to. A restart and its word are listed together or not at all, and the word is not counted among the instructions asked for. That holds while the first three bytes at the restart’s address, in the memory the listing reads, are the lower ROM’s: the ROM itself, or the RAM beneath it, where the firmware keeps a copy. A program that disables the ROM may put a restart of its own there, as Aliens makes RST &28 its key test, and its restart is listed alone. make test-firmware holds a listing of each CPC’s whole main jumpblock, a run of LOW JUMPs and FAR CALLs, to its entries, through the ROM and beneath it. On a CPC, an instruction that jumps or calls to an entry of the firmware’s jumpblocks, the main one from &BB00, the high kernel’s from &B900 and the indirections from &BDCD, carries the entry’s name as SOFT 968 lists it, KM WAIT KEY for CALL &BB18, wherever the machine’s firmware has that entry: a cpc464’s firmware, V1.0, has none of the main jumpblock’s from KM SET LOCKS on, nor KL SCAN NEEDED or KM SCAN KEYS, and a cpc664’s, V1.1, has no KL BANK SWITCH. The name belongs to the address, and is given whatever the memory there holds, because a program patches an entry to take it over, as AMSDOS takes over the cassette’s, and a call to the entry is a call to it still. make test-disassembly holds every instruction of the 48K ROM to The Complete Spectrum ROM Disassembly, and every opcode to SkoolKit’s own disassembler, save where the player’s names differ from SkoolKit’s, and where SkoolKit leaves a DD or FD that changes nothing as a byte of its own, which the listing joins to the instruction after it as the processor does; it holds every name to the number and the address SOFT 968 gives the entry and to the spacing SOFT 968 gives it most, and the main jumpblock’s entries each version has to what SOFT 968 says of each. make test-firmware holds which addresses are named to the jumpblocks each CPC’s firmware sets up as it starts.

Keys and typing

A machine is reached through its keyboard as a person reached it, a key at a time, and what a key means is its firmware’s business. run takes keys to hold down from its start to its end, on top of whatever the keyboard holds already, and leaves the keyboard holding what it held before; a stretch of a game played with a direction and fire held is a run with those keys, and the next stretch another run. A key goes by the name the player prints on it, after the keys SOFT 968, Amstrad’s firmware manual, draws for a CPC, SPACE, COPY, f5, and (FIRE 2) for what the manual calls joystick 0’s main button, though some games read (FIRE 1), and after the legends on a Spectrum’s, CAPS and SYM among them. A CPC’s key may also be given by the number its firmware gives it, its line of the matrix times eight and its bit, which is the number a game’s own code tests.

type types text as the emulator’s command line types it with --type: each character’s key held down, with whatever shift it needs, for 3 standard frames, then let go for 3 on a CPC and 6 on a Spectrum. Those are the command line’s counts, measured there: a CPC’s firmware marks a key let go only once two of its scans, a fiftieth of a second apart, have missed it, which also leaves a key let go at the end of a run down to the firmware there and a frame later, and up a frame after that, as the tests find with KM TEST KEY; and a Spectrum’s takes the same key again only once five of its scans have passed. A newline is RETURN or ENTER, and on a CPC a tab, an escape and a backspace are TAB, ESC and DEL. A shift goes down beside its key, so a program that reads the keyboard’s matrix itself, rather than asking the firmware, finds one key more than the character typed, and text for such a program is typed in lower case. A text is ASCII, at most 250 characters on a CPC and 166 on a Spectrum, 1500 frames’ worth, and one holding a character no key types, alone or with a shift, is refused before a key goes down; a key that types no character, or a character outside ASCII such as a CPC’s £, is held through run. Keys the keyboard holds already, as set writes them, stay held through the typing, where the command line lets every key go. The keys go down and come up between instructions, a few ticks at most from where the command line puts them, counting from where the typing began, and the typing ends at the end of the instruction standing when its frames’ ticks have run out, or where a stretch of it ends inside a chain of prefixes, as a run does. The tests type PRINT 2+2 at each CPC and p2+2 at the Spectrum, after the frames the command line waits, and the screen’s memory is what the command line’s typing leaves there; the firmware’s answer, read in its own font, is 4. They hold keys through a run and BASIC’s INKEY reads them as the CPC 6128’s manual says it does, and a Spectrum held at CAPS and SPACE breaks into its program with report L.

A Spectrum’s ROM reads a letter typed where the cursor shows K as a keyword, so p at the start of a line is PRINT and j is LOAD. Neither firmware takes a key before it has started: the command line waits 78 frames on a CPC, and 128 on a Spectrum, whose ROM takes none before frame 85.

Discs and tapes

A disc or a tape is given as a file, which is read whole and copied into the machine, and named in the answer by its SHA-256, with the tick at which it went in. Nothing the machine does reaches the file.

disc_insert puts a DSK image, of the original layout or the extended one, in a CPC’s drive A or B, with room past it for every track the medium can hold to be formatted once more, a track the image never had among them, as the command line gives one with --disc. A disc may go in with its write-protect tab set, which the drive tells the disc controller as a real drive does, and the controller then writes and formats nothing on it. What the machine writes lands in its copy, and is gone once another disc takes the copy’s place, as a game’s second side does when the game asks for it, save what a state kept while it stood in the drive brings back as it stood then. A drive whose motor turns reports a disc ready at the third index pulse after it goes in, 600 ms, which the emulator’s drive takes for the CPC6128 service manual’s two revolutions; AMSDOS leaves the motor turning for 5 seconds after it reads, and a disc put in meanwhile and asked for sooner is reported missing. disc_save writes the disc in a drive to a new file, as the command line does with --save-disc: an image of the extended layout as the emulator writes one, naming itself its creator and recording where each sector lies, so that a disc nothing wrote to may come out with a hash other than that of the file it went in from, and the answer says whether the machine wrote to it since it went in. The image holds the disc as it stands at that tick, and a save the program has not finished is on it as far as it has come. A cpc664 and a cpc6128 take discs. A cpc464 does not: it is built here as the command line builds one given no disc, without the disc interface, and nothing here fits one afterwards. The tests save a one-line program to a blank disc they write in SOFT 968’s DATA format, and AMSDOS catalogues it as the CPC 6128’s manual shows it does, EXAMPLE.BAS 1K and 177K free; the same file, put in again, is still blank, and the image disc_save wrote, put in after it, is catalogued as the disc was; and an image disc_save writes after a program is typed and saved is, byte for byte, the one the command line writes with --save-disc after the same typing. Put in write-protected, it takes no SAVE: the controller reports the drive’s tab to a program that asks, and refuses the write, and AMSDOS says Drive A: disc is write protected, as its own message table spells it.

tape_insert puts a tape in a machine’s deck: a .tzx on either machine, a .cdt on a CPC and a .tap on a Spectrum, whose bare blocks carry only the Spectrum ROM’s timings. A rewound cassette has leader tape in front of its recording, which an image seldom keeps whole, and the emulator plays it first, as 5.7 seconds of silence, less whatever silence the image keeps before its first edge.

A Spectrum has no motor line, so its deck presses PLAY as the tape goes in, and the tape plays from that tick on, whatever the machine does; after the leader tape, a header’s pilot tone lasts 5 seconds. So a tape goes in just before the program asks for it, as LOAD "" does. The tests write a tape of one BASIC program as the 48K reference describes what SAVE puts on tape, hear its first edge 5.7 seconds after PLAY, and LOAD "" reads it and runs it from its autostart line.

A CPC’s tape moves only while the machine runs the cassette motor, on bit 4 of its PPI’s port C, and its reel follows the line 50 ms late, from rest as the tape goes in. That is the floor that Opera Soft’s cassettes set and no measurement, so a tape that wants its reel to run on longer after the motor stops may fail here where a real deck reads it. The firmware runs the motor only to read or write, so a tape may go in at any time before RUN" asks for it; a cpc664 and a cpc6128 read tapes once |TAPE has turned the firmware’s cassette routines back to the cassette, and the firmware asks for PLAY and a key before it starts the motor. A TZX block that stops a Spectrum’s tape is passed over on a CPC, as the format’s notes for the Amstrad ask: a pause of none “SHOULD NOT ‘Stop the Tape’”, and a stop “if in 48K Mode” “MUST NOT have any effect”. The tests write a tape of one binary file as SOFT 968 describes the cassette manager’s records, find it standing while the motor is off, for longer than the leader tape and its first record, and hear its first edge the reel’s lag and the leader tape after the motor starts; each CPC’s RUN" reads it, the tape put in at power-on, and runs it from its entry address.

Saving a machine

snapshot_save writes a machine to a file in the format the command line writes with --save and machine_open reads: an SNA of version 1 for a CPC, the 48K .sna for a Spectrum. It writes only where no file is, so that nothing is ever written over, and the answer names the file by its SHA-256, with the machine’s tick. A snapshot keeps less than the machine, as the last section says; a state file keeps all of it, for the build that wrote it, as the next section says. A snapshot keeps the machine’s RAM whole, so the file is an image of it for tools of the client’s own: a CPC’s all of it after a header of 256 bytes, by the RAM’s own addresses, and a Spectrum’s after a header of 27, from &4000, save the two bytes below the stack pointer, which carry the program counter. One byte of a CPC’s header is written against the format when its sound chip is unselected, by the emulator and so by the server: the format’s notes ask for the sound chip’s selected register at &5A to be 0 to 15, and a chip left unselected is written there as its psg.selected plus 16; a byte above 15 there opens as a chip unselected, with the register in its low four bits. The tests find both, and hold a snapshot the server saves to the one the command line saves of the same machine, byte for byte, and one the command line runs on from to where the server, opening it, runs on to.

Keeping a machine

A machine can be kept as it stands and put back there, as often as an agent wants to try one thing and then another from the same place. state_keep keeps everything the machine holds: its processor inside an instruction if a run left it in a chain of prefixes, where no snapshot has room for it, its picture, each disc as it has been written, the tape where it has played to and how far a CPC’s reel has come up to speed, its tick and its journal. state_restore puts the machine back exactly so, its discs among it though another disc has taken a drive since, and a run from there goes on as it went on from the moment kept. The tests hold a CPC kept while AMSDOS writes its disc, with its disc controller inside a command, another kept partway through RUN", and a Spectrum kept partway through LOAD "" to the machines they ran on to, and a stretch gathered again from a state to the stretch gathered live. Restoring a state drops no other: each is a branch to come back to. state_drop lets one go, and machine_list names those held of each machine.

A state is held in the server’s memory, beside its machine, and is restored into that machine alone, because it holds the machine’s own pointers, a CPC’s into itself, and at its buffers; it goes with its machine, or with the server. States kept while a disc does not change share one copy of it.

state_save writes a state to a file, and machine_open, given the file, builds a new machine as the state was, so that a machine outlasts the server that ran it and one worker begins where another left off.

The file holds no pointer. Its first line is JSON: the build that wrote it, the machine, the tick, the files the machine was built from, the journal, and every field of the machine, by the names inspect reads them by, and of the disc in each drive and of the deck and its reader, by the names the emulator’s headers give them. The buffers those point at follow it byte for byte: the RAM, the picture, each disc’s image and the tape’s.

A machine read from the file is built as any is, its 6845 of the type the state’s was, then written field by field through the checks set makes, so that every pointer is the new machine’s own; the drive the disc controller had chosen and the disc in each drive are set as the file says, and a CPC’s memory map is worked out again. A disc read from it is held to the bounds the emulator’s floppy.c trusts of a disc it laid out itself. A tape is opened again and played from its start, pulse by pulse, until its reader stands as the file’s stood; a deck takes at most one pulse a tick, so its reader comes there within as many pulses as the state has ticks. Only the build that wrote the file opens it, on the firmware the state was kept on, by its hash, because a field is read by its name and nothing promises another build gives a name the same meaning; on any other build the journal in the file’s first line makes the machine again. The machine takes the state’s journal and the files its journal names, the firmware and the snapshot the state’s machine was built from, where machine_open’s answer names the files read to build this one, the firmware and the state file.

The tests build a CPC kept while AMSDOS writes its disc, another kept partway through RUN" and a Spectrum kept partway through LOAD "" from their files in another server, and hold them to the machines they ran on to; they find a 6128 read from its file cataloguing its disc and saving across two tracks with its disc controller as AMSDOS set it up, which a snapshot loses, and a run by frames from a file ending where it ends on the machine kept.

The journal

A finding is to leave as something the machine can be asked again, and journal is that: the build that runs a machine, the files it was built from, each by its SHA-256, and every request that stood it up or changed it, in order, the first of them the machine_open that stood it up, each with the machine’s tick as it began and the arguments it was given, save that an address, a port or a byte a condition’s value or value_to gives is written after &, a register of the PSG or the 6845 as a number, a range of one address, register or byte as that one, a test of every byte and a changes that is false left out, when’s bytes as pairs of capital digits, or ??, a space between each, a CPC’s machine_open given the type of its 6845 even where it named none, and a path absolute, and a snapshot, a disc or a tape read named by its SHA-256 as well, and a file a run or a call wrote named by what it holds. Reads are not entered, nor are snapshots and discs written out, nor states kept, written or let go; a restore makes the journal the state’s, and a machine stood up from a state file carries the state’s on, so the journal is always the way from the machine’s opening to where it stands. The same requests made in the same order, on the same build and with the same files, make a machine that stands as the first stands, which the tests do, with a branch a restore left behind; a request that wrote a file writes one that holds the same, at a path that names nothing yet. A long journal is answered a page at a time, as many entries as 8192 characters hold, and one at least.

The files a machine is built from

machine_open answers with every file read to build the machine, its firmware and its snapshot or its state file, each by its path and the SHA-256 of what it held, and disc_insert and tape_insert name their files the same way. A finding names its copy by that hash, and a file changed between two openings gives two hashes. A path is read and written on the file system the server runs on, from its working directory if it is relative, and the answers and the journal name the file by its absolute path, so that a journal is made again from anywhere; a file that cannot be reached is refused with that path and the working directory. Anything there but a file is refused unread, firmware, snapshot, state file, disc or tape, and so is a snapshot longer than 49179 bytes for a Spectrum, the one length its snapshot has, or than 8MB for a CPC, whose version 3 may carry chunks after its memory, a disc or a tape longer than 16MB, and a state file longer than 128MB, or whose first line is longer than 32MB.

The name of the build

The server names itself and the build in the _meta of its answers to server/discover, tools/list and tools/call, as the 2026-07-28 revision asks of every result and the older ones allow, and in the serverInfo of its answer to initialize. A ping’s answer does not, nor an error, nor the empty answers the protocol’s library gives for what the server does not offer. The name is the package’s own version, then the commit of this repository and the commit of the emulator it was compiled from.

0.0.0+mcp.<commit>.dirty.emulator.<commit>

.dirty follows a commit whose files differ from the build’s, an untracked file among them, or an ignored file of a kind the build compiles: here, the sources, the bridge, the build script and the files that pin the crates and the toolchain; in the emulator, its src/. A change anywhere else in either repository does not mark the build, because nothing compiled from it changed. Where a repository is not a checkout of its own, the commit is none. A finding can be made again only on a build that is unmarked and names commits the published repositories hold; one made on any other build is made again on such a build before it leaves.

What it cannot do