beebjit-MCP is primarily an MCP server for AI agent clients, exposing BBC Micro emulation through stdio JSON-RPC. The same package also exposes a Python library, where the same operations are available as methods on an in-process driver class. Both share the same driver, the same beebjit subprocess, and the same per-operation behaviour. The MCP tool reference is at tool-reference.md; this page covers the library.
Use the beebjit-MCP server for:
-
AI agent control. The tools and their docstrings are written for that case.
-
Callers outside Python. MCP's stdio JSON-RPC works from any language.
Use the library for:
-
Test harnesses. A pytest run that needs to boot a BBC, exercise a disc image, and assert on the result runs in-process with no JSON-RPC framing in between.
-
Scripted automation. A Jupyter notebook, a CLI tool, a one-off batch job. The class is small and reads naturally as Python.
-
Direct control over timing and command ordering.
sendCommandaccepts any debugger command beebjit understands, without waiting for a typed wrapper to land.
The two are not exclusive. Both can run against the same project; each beebjit subprocess is independent.
The library requires Python 3.10 or later and ships in the beebjit-mcp package. There is no separate install.
pip install beebjit-mcpA beebjit binary is required at runtime, since the library spawns it as a subprocess. The installation guide covers binary install.
For binary discovery, BeebjitDriver.fromEnvironment looks at $BEEBJIT first then beebjit on $PATH. To pin a specific binary, pass the path to the BeebjitDriver constructor directly.
A complete session that boots a BBC B, types a one-line BASIC program, runs it, and reads the screen back:
from beebjit_mcp import BeebjitDriver, BeebModel
from beebjit_mcp.screen import decodeMode7
with BeebjitDriver.fromEnvironment(model=BeebModel.B) as bbc:
bbc.runCycles(5_000_000)
bbc.typeText('PRINT "HELLO"\n')
bbc.runCycles(2_000_000)
page = bbc.captureMode7Bytes()
for row in decodeMode7(page):
print(row)fromEnvironment locates the beebjit binary from $BEEBJIT first and then beebjit on $PATH, mirroring the discovery the MCP server uses. To pin a specific binary in code, pass the path directly: BeebjitDriver("/path/to/beebjit", model=BeebModel.B).
The with block guarantees teardown. The five-million-cycle wait gives the MOS time to finish booting and land at the BASIC > prompt before the first keypress arrives. typeText resolves each character to a matrix key event with the right SHIFT handling. captureMode7Bytes grabs the teletext framebuffer page; decodeMode7 turns those bytes into 25 rows of 40 characters.
-
Methods return native Python values, not JSON dicts.
readMemoryreturnsbytes,readRegistersreturnsdict[str, int | str],captureScreenreturns a(bgra, width, height)tuple. The MCP server wraps these into JSON for its tool layer; library callers see the raw values. -
Addresses and cycle counts are Python ints. Both
31744and0x7C00parse to the same value. Hex literal form keeps BBC addresses self-documenting. -
Errors raise exceptions, never an
okflag.BeebjitErrorfor protocol or subprocess failures,ValueErrorfor argument validation,TimeoutErrorfor host-side waits. The MCP tool layer translates these to JSON-RPC errors; library callers catch them directly. -
One driver instance owns one beebjit subprocess. Methods on a single driver are not safe to call from multiple threads concurrently. See Concurrency.
-
This documentation uses
&ADDRfor BBC memory addresses, matching the BBC Micro User Guide. Source code uses0x. -
BeebjitDriver,BeebjitError, andBeebModelare re-exported at the top of the package.from beebjit_mcp import BeebjitDriverandfrom beebjit_mcp.driver import BeebjitDriverboth work. Helpers live in their submodules (beebjit_mcp.keyboard,beebjit_mcp.screen,beebjit_mcp.image).
A complete end-to-end function that boots a BBC, runs a coloured MODE 7 BASIC program, and writes a PNG of the result lives at python-example.md.
| Method | Category | Purpose |
|---|---|---|
BeebjitDriver(...) |
Lifecycle | Configure a driver for a BBC session |
BeebjitDriver.fromEnvironment |
Lifecycle | Auto-discover the binary and construct a driver |
start |
Lifecycle | Spawn the beebjit subprocess |
close |
Lifecycle | Tear the subprocess down |
reset |
Lifecycle | Hard-reset the BBC, optionally SHIFT+BREAK autoboot |
coldBootWithAutoboot |
Lifecycle | Cold-boot autoboot from cycle zero |
loadDisc |
Disc | Mount a disc image into a drive at runtime |
runCycles |
Execution | Advance the emulator by exactly N BBC cycles |
typeText |
Input | Type an ASCII string (CAPS LOCK on) |
typeTextRaw |
Input | Type an ASCII string preserving case (CAPS LOCK off) |
tapKey |
Input | Press, hold, release, idle for one key |
tapShiftedKey |
Input | tapKey with left-SHIFT held across the press |
keyDown |
Input | Low-level matrix key-down event |
keyUp |
Input | Low-level matrix key-up event |
readRegisters |
State reads | Return 6502 register state |
readMemory |
State reads | Read a contiguous block of BBC RAM |
disassemble |
State reads | Disassemble 6502 instructions |
captureMode7Bytes |
State reads | Capture the MODE 7 framebuffer in display order |
captureScreen |
State reads | Capture the rendered framebuffer as raw BGRA |
writeMemory |
State writes | Poke bytes into memory |
stderrTail |
Diagnostics | Return the bounded ring of beebjit stderr |
sendCommand |
Diagnostics | Send one raw debugger command |
asciiToKeys |
Keyboard helpers | Translate a string to (key, shifted) tuples (CAPS LOCK on) |
asciiToKeysRaw |
Keyboard helpers | Translate a string preserving case (CAPS LOCK off) |
resolveKeyName |
Keyboard helpers | Resolve a symbolic key name to a BBC scancode |
decodeMode7 |
Screen helpers | Decode a 1000-byte MODE 7 framebuffer into 25 rows |
rotateMode7Page |
Screen helpers | Rotate a 1024-byte teletext page into display order |
mode7TextContains |
Screen helpers | True if a needle appears anywhere in the decoded screen |
bgraToPng |
Image helpers | Encode raw BGRA pixels as PNG bytes |
Configure a driver for a BBC session. The constructor does not spawn beebjit; start does, or the context manager calls it on entry.
Parameters
-
binaryPath(Path): Path to the beebjit executable. For automatic discovery from$BEEBJITor$PATH, usefromEnvironmentinstead. -
model(str | BeebModel, defaultBeebModel.B): BBC hardware configuration. Either aBeebModelmember or its wire string. The four values are:BeebModel.B("b"): BBC B with MOS 1.20 and the 8271 floppy controller.BeebModel.MASTER("master"): Master 128 with MOS 3.20.BeebModel.MOS35("mos35"): Master 128 with MOS 3.50.BeebModel.COMPACT("compact"): Master Compact.
Disc-image format follows from the model. BBC B and the two Master 128 variants read DFS images (
.ssdand.dsd); Master Compact reads ADFS images (.adland.adf). -
cycles(int, default10**12): beebjit-cyclescap, a circuit-breaker against orphaned processes. The default is one trillion BBC cycles, effectively unbounded for normal sessions.
Returns
A configured BeebjitDriver instance. No subprocess is running yet.
Errors
ValueErrorifmodelis neither aBeebModelmember nor a recognised wire string.
Example
import os
from pathlib import Path
from beebjit_mcp import BeebjitDriver, BeebModel
bbc = BeebjitDriver(
Path(os.environ["BEEBJIT"]),
model=BeebModel.MASTER,
cycles=100_000_000,
)Notes
The constructor is cheap. The expensive work is in start, which spawns beebjit and blocks until the first debugger prompt. Use the context manager unless manual lifecycle is unavoidable.
Construct a driver after locating the beebjit binary automatically. Classmethod alternative to the explicit-path constructor.
Parameters
-
model(str | BeebModel, defaultBeebModel.B): BBC hardware configuration. Same shape as the constructor; seeBeebjitDriverfor the four values. -
cycles(int, default10**12): beebjit-cyclescap. Same meaning as the constructor.
Returns
A configured BeebjitDriver, ready for start or use as a context manager.
Errors
-
FileNotFoundErrorif neither$BEEBJITnorbeebjiton$PATHresolves to an executable file. -
ValueErrorifmodelis neither aBeebModelmember nor a recognised wire string.
Example
from beebjit_mcp import BeebjitDriver, BeebModel
with BeebjitDriver.fromEnvironment(model=BeebModel.B) as bbc:
bbc.runCycles(5_000_000)Notes
Discovery order: $BEEBJIT first, then beebjit on $PATH. Matches the discovery the MCP server runs at startup.
For callers that need the path without constructing a driver, the underlying discoverBinary() function is also available: from beebjit_mcp.driver import discoverBinary.
Spawn the beebjit subprocess and block until the first debugger prompt.
Parameters
initialPromptTimeout(float, default10.0): seconds to wait for beebjit to reach its first(6502db)prompt before raising.
Returns
None. The driver is ready for commands when the call returns.
Errors
-
BeebjitErrorif the subprocess exits before the prompt arrives. The error message includes the stderr tail. -
TimeoutErrorif beebjit is slow to reach cycle zero withininitialPromptTimeout.
Example
bbc = BeebjitDriver(Path(os.environ["BEEBJIT"]))
bbc.start()
try:
bbc.runCycles(5_000_000)
finally:
bbc.close()Notes
The context manager calls start on entry and close on exit, including the exception path. Use with BeebjitDriver(...) as bbc: unless the surrounding code rules it out.
Tear the beebjit subprocess down and join the reader threads. Idempotent.
Returns
None. The driver is left in a stopped state; a fresh BeebjitDriver is required for another session.
Example
bbc = BeebjitDriver(Path(os.environ["BEEBJIT"]))
bbc.start()
bbc.close()
bbc.close() # second call is a no-opNotes
The teardown sends q to the debugger, waits up to five seconds for clean exit, escalates to SIGKILL if needed, and removes the per-session temp dir. Idempotency keeps retry paths and finally-blocks simple.
The driver does not implement __del__, so garbage collection does not reclaim the subprocess. An abandoned driver leaks a beebjit process until the -cycles cap trips.
Hard-reset the BBC, equivalent to a user pressing BREAK. The 6502 cycle counter wraps, BASIC variables clear, and the boot banner reappears. The driver and its session stay valid.
Parameters
autoboot(bool, defaultFalse): Hold left-SHIFT across BREAK and through MOS's keyboard-matrix scan window so the OS picks up SHIFT+BREAK and runs the inserted disc's!BOOT.
Returns
None. The BBC is at a fresh BASIC prompt (or, with autoboot=True, the disc's !BOOT has already run).
Example
bbc.reset() # back to BASIC prompt
bbc.reset(autoboot=True) # SHIFT+BREAK to autoboot a mounted discNotes
The reset taps F12 to assert the 6502 RESET line, then advances the BBC for a fixed cycle budget generous enough for MOS init to settle on every supported model.
For mid-session "load a new disc and boot it" in one gesture, compose loadDisc with reset(autoboot=True). For cold-boot autoboot from cycle zero, use coldBootWithAutoboot.
Hold SHIFT in the matrix from cycle zero, mount a disc, advance cycles. Mirrors a real user holding SHIFT before powering on with a disc in.
Parameters
-
drive(int): BBC disc drive to mount into. Either0or1. -
path(Path | str): Absolute path to a disc image (DFS.ssd/.dsd, or ADFS.adl/.adf). -
writeable(bool, defaultFalse): Cut the write-protect notch so the BBC can write to the in-memory image. -
mutable(bool, defaultFalse): Flush in-memory writes back to the host file. Requireswriteable=True. -
settleCycles(int, default20_000_000): Cycles to advance after the SHIFT-hold so the autoboot keyboard scan and the disc's!BOOTfinish.
Returns
None. SHIFT is released after the settle window; the BBC is in the post-autoboot state.
Errors
-
ValueErrorifmutable=Trueandwriteable=False, ordriveis not 0 or 1. -
BeebjitErrorif the underlyingloadDiscis rejected by the fork's pre-check.
Example
with BeebjitDriver(Path(os.environ["BEEBJIT"])) as bbc:
bbc.coldBootWithAutoboot(0, "/path/to/game.ssd")Notes
Must be called immediately after start, while the CPU is still at cycle 0. Calling it on a session that has already advanced cycles silently misses the boot keyboard-scan window because MOS init has already run.
Works across every supported model, including Master 128 / MOS 3.50, which ignores a mid-session SHIFT+BREAK. For mid-session autoboot, use loadDisc followed by reset(autoboot=True) instead.
Mount a disc image into a drive at runtime. The BBC keeps its current state; the new disc becomes available for the next OS read.
Parameters
-
drive(int): BBC disc drive. Either0or1. -
path(Path | str): Absolute path to a disc image (DFS.ssd/.dsd, or ADFS.adl/.adf). -
writeable(bool, defaultFalse): Cut the write-protect notch so the BBC can write to the in-memory image. -
mutable(bool, defaultFalse): Flush in-memory writes back to the host file. Requireswriteable=True.
Returns
None. The disc is mounted on success.
Errors
-
ValueErrorifmutable=Truewithoutwriteable=True, ordriveis not 0 or 1. -
BeebjitErrorif beebjit's own pre-checks reject the mount (extension, slot count, file readability), if the binary is too old to recognise theloaddisccommand, or if the response cannot be parsed.
Example
bbc.loadDisc(0, "/path/to/game.ssd")
bbc.reset(autoboot=True)Notes
writeable controls whether the BBC can modify the in-memory image; mutable controls whether those modifications persist back to the host file. The two flags compose: writeable=True, mutable=False is "writes survive in this session only, never reach disk", useful for save-game probes that should not perturb the source file.
The mount does not trigger a reset or autoboot. For mount + autoboot in one call from cycle zero, use coldBootWithAutoboot; for mid-session autoboot, compose with reset(autoboot=True).
Advance the emulator by exactly n BBC cycles using a tick-anchored breakpoint.
Parameters
-
n(int): BBC cycles to advance. Non-positive values return immediately. -
timeout(float, default30.0): host-side timeout on the underlyingc(continue). Covers many billions of BBC cycles at-faston a modern host. For longer runs, chunk from the caller side.
Returns
None. The post-run cycle counter can be read separately via readRegisters.
Errors
-
BeebjitErroron a pipe or protocol failure. -
TimeoutErrorifcdoes not return withintimeout.
Example
bbc.runCycles(5_000_000)
regs = bbc.readRegisters()
print(regs["cycles"])Notes
Cycle counting is anchored against beebjit's global total_timer_ticks counter (read live via eval ticks on each call). The driver reads ticks, computes target = ticks + n, sets breakat target, and issues c. Anchoring against ticks rather than the 6502-relative cycles= field is what makes the call survive a soft reset, since cycles= rebases on Break while ticks are monotonic. Chained calls do not accumulate drift.
beebjit can overshoot the breakpoint by a handful of instructions. The cycles field in readRegisters() reports the exact 6502-relative stopping point and may differ from n by a few cycles.
For "run until something happens" patterns, the MCP server has run_until_text and run_until_prompt tools that wrap a polling loop over captureMode7Bytes. From Python, the same pattern is one short loop:
from beebjit_mcp.screen import mode7TextContains
while not mode7TextContains(bbc.captureMode7Bytes(), "READY"):
bbc.runCycles(500_000)Type an ASCII string as BBC keypresses. Assumes CAPS LOCK ON (the cold-boot default), so lowercase input is silently upper-cased on the way to the matrix.
Parameters
-
text(str): ASCII text to type. A trailing\ntriggers RETURN. -
holdCycles(int | None, defaultNone): Per-key HOLD cycles.Noneresolves toBeebjitDriver.DEFAULT_KEY_HOLD_CYCLES(5,000,000). -
gapCycles(int | None, defaultNone): Per-key GAP cycles.Noneresolves toBeebjitDriver.DEFAULT_KEY_GAP_CYCLES(5,000,000).
Returns
None. The emulator has advanced by len(text) * (hold + gap) BBC cycles plus SHIFT overhead where applicable.
Errors
UnsupportedCharError(aValueErrorsubclass, frombeebjit_mcp.keyboard) for characters with no BBC matrix mapping.
Example
bbc.typeText('PRINT "HELLO"\n')Notes
Each character is translated to a BBC matrix key, tapped with SHIFT held where the BBC layout requires it, and separated from the next character by a cycle gap tuned for 100% reliability against the MOS keyboard ISR. See keypress-timing.md for the reasoning behind the defaults.
For deterministic case control on screen, call set_caps_lock(False) at the MCP layer (or compose the equivalent matrix tap from Python) and use typeTextRaw instead.
Type an ASCII string preserving case. Requires CAPS LOCK OFF; a cold-boot BBC has CAPS LOCK ON and will otherwise invert every letter.
Parameters
-
text(str): ASCII text to type. A trailing\ntriggers RETURN. -
holdCycles(int | None, defaultNone): Per-key HOLD cycles.Noneresolves toBeebjitDriver.DEFAULT_KEY_HOLD_CYCLES. -
gapCycles(int | None, defaultNone): Per-key GAP cycles.Noneresolves toBeebjitDriver.DEFAULT_KEY_GAP_CYCLES.
Returns
None.
Errors
UnsupportedCharErrorfor characters with no BBC matrix mapping.
Example
bbc.typeTextRaw('print "hi"\n')Notes
Like typeText, but the case of each letter dictates whether SHIFT is applied. Lowercase stays lowercase on screen; uppercase becomes SHIFT-held so the BBC matrix produces uppercase.
The driver does not flip CAPS LOCK for you. The MCP server's set_caps_lock tool reads &025A bit 4 and taps key 135 idempotently; from Python the same gesture is bbc.readMemory(0x025A, 1) plus a conditional bbc.tapKey(135) (CAPS LOCK).
Press, hold for HOLD cycles, release, then idle GAP cycles. The base building block for reliable matrix taps.
Parameters
-
key(int): BBC scancode. ASCII codes work for letters/digits/direct punctuation; symbolic keys live inbeebjit_mcp.keyboardasBBC_KEY_*constants (BBC_KEY_RETURN,BBC_KEY_ESCAPE,BBC_KEY_F0etc.). -
holdCycles(int | None, defaultNone): Cycles to hold the key down.Noneresolves toDEFAULT_KEY_HOLD_CYCLES. -
gapCycles(int | None, defaultNone): Cycles to idle after release before returning.Noneresolves toDEFAULT_KEY_GAP_CYCLES.
Returns
None.
Example
from beebjit_mcp.keyboard import BBC_KEY_RETURN
bbc.tapKey(BBC_KEY_RETURN)
bbc.tapKey(ord("Y"))Notes
Four-step tap: down, hold, up, gap. The GAP phase lets the BBC MOS clear its CA2-mask state so the next press can raise a fresh interrupt. Shortening HOLD or GAP below the defaults risks the MOS keyboard ISR missing the press; the keypress-timing page documents the empirical floor.
tapKey with left-SHIFT held across the key's hold window.
Parameters
-
key(int): BBC scancode for the non-SHIFT key. -
holdCycles(int | None, defaultNone): Cycles to hold the main key. Defaults as fortapKey. -
gapCycles(int | None, defaultNone): Cycles to idle after release. Defaults as fortapKey.
Returns
None.
Example
bbc.tapShiftedKey(ord("2")) # types " on a BBC layoutNotes
SHIFT lives on row 0 of the BBC matrix and does not itself raise the keyboard CA2 interrupt, so it only needs to be in the matrix at the moment the scan sees the main key pressed. The GAP is applied once at the end after SHIFT is released.
Inject a raw matrix key event. No automatic timing, no SHIFT handling, no gap.
Parameters
key(int): BBC scancode (0-255). ASCII codes for letters/digits/direct punctuation;BBC_KEY_*constants frombeebjit_mcp.keyboardfor symbolic keys;BBC_KEY_RELEASE_ALL(255) to release every held key in one event.
Returns
None.
Example
from beebjit_mcp.keyboard import BBC_KEY_SHIFT_LEFT, BBC_KEY_BREAK
bbc.keyDown(BBC_KEY_SHIFT_LEFT)
bbc.keyDown(BBC_KEY_BREAK)
bbc.runCycles(200_000)
bbc.keyUp(BBC_KEY_BREAK)
bbc.runCycles(10_000_000)
bbc.keyUp(BBC_KEY_SHIFT_LEFT)Notes
These events go straight to the BBC matrix. Pair with runCycles for timing, or use tapKey for a reliable single press with sensible defaults. The example above is the cycle-anchored SHIFT+BREAK that reset(autoboot=True) performs internally.
Return 6502 register state.
Returns
A dict[str, int | str] with:
-
A,X,Y,S(int): Standard 6502 register values. -
F(str): beebjit's whitespace-padded 8-character flag string verbatim. Each slot is one flag; unset flags show as space, set flags carry their letter, and the unused B slot holds a literal1. Scan for the letter of interest rather than relying on fixed column positions. -
PC(int): 16-bit program counter. -
cycles(int): 6502-relative cycle counter (cycles=fromr). Rebases near zero on every BBC Break. For cross-call cycle arithmetic that survives a soft reset, userunCycleswhich anchors against beebjit's monotonic tick counter internally.
Errors
BeebjitErrorif the register line cannot be parsed.
Example
regs = bbc.readRegisters()
print(f"PC={regs['PC']:#06x} A={regs['A']:#04x} F={regs['F']!r}")Read a contiguous block of BBC RAM.
Parameters
-
addr(int): 16-bit BBC address (0-65535). -
length(int): Number of bytes to read.
Returns
bytes. Exactly length bytes starting at addr. The MCP server wraps this into a {"hex": ..., "ascii": ...} shape; the library hands back the raw bytes for direct slicing or feeding into a struct unpack.
Errors
-
ValueErrorifaddris outside the 6502 address space,lengthis negative, oraddr + lengthwould exceed0x10000. The driver does not wrap silently. -
BeebjitErrorif amcommand returns no parseable memory lines.
Example
banner = bbc.readMemory(0x7C00, 40)
print(banner.decode("ascii", errors="replace"))Notes
Each beebjit m call returns 64 bytes. The driver loops with the next unread address until it has the requested length, then trims.
Disassemble 6502 instructions starting at addr.
Parameters
-
addr(int): 16-bit address to start disassembly at. -
count(int, default20): Number of instructions to return. Capped at 20 (beebjit's native batch size).
Returns
list[dict[str, int | str]]. Each dict has:
addr(int): Address of the instruction.info(str): beebjit's per-line tag ("ITRP"for interrupt,"JIT"for compiled code, sometimes empty).text(str): Mnemonic and operands as beebjit prints them.
Errors
-
ValueErrorifaddris out of range orcountis outside 1..20. -
BeebjitErrorif no disassembly lines are parsed.
Example
for ins in bbc.disassemble(0xE000, count=5):
print(f"{ins['addr']:#06x} {ins['info']:>5} {ins['text']}")Notes
For longer runs, loop from the caller side by re-dispatching at the next unread address. beebjit does not emit raw opcode bytes in its disassembly output; pair with readMemory at the same address if needed.
Capture the MODE 7 framebuffer in display order, handling hardware scroll.
Returns
bytes. Exactly 1000 bytes (MODE7_BYTES). Byte 0 is the top-left cell as painted by the CRTC; the result feeds directly into decodeMode7.
Example
from beebjit_mcp.screen import decodeMode7
for row in decodeMode7(bbc.captureMode7Bytes()):
print(row)Notes
The MODE 7 framebuffer sits in a 1024-byte page at &7C00. Only 1000 bytes are visible at any time. Hardware scrolling advances the display-start pointer; the MOS caches it at &0350/&0351. The driver reads the whole 1024-byte page plus the pointer, rotates the page into display order, and returns the 1000 display bytes.
Pre-MOS-init the pointer at &0350/&0351 is uninitialised RAM. In that state the driver falls back to the page base address, so polling during boot does not raise; the decoded text is arbitrary but the loop keeps iterating.
This is MODE-7-only. In a bitmapped mode (MODE 0-6) the returned bytes are not meaningful text. Use captureScreen for the rendered display in any mode.
Capture the rendered framebuffer as raw BGRA pixel bytes.
Returns
tuple[bytes, int, int]: (bgra_bytes, width, height). For a PNG, pass these to bgraToPng. The raw form is the building block; callers who prefer Pillow, numpy, or a direct file write skip the PNG step entirely.
Errors
BeebjitErrorif the binary lacks the render buffer (typically: launched without-headless-render, or a build that does not support thesavescreendebugger command), or if the announced size disagrees with the file written.
Example
bgra, width, height = bbc.captureScreen()
print(f"{width}x{height}, {len(bgra)} bytes")Notes
width and height come from beebjit's announced render geometry, not an MCP-side guess. The pixel data is the rendered display, not the raw BBC framebuffer bytes: hardware scroll, palette, and any video ULA effects are baked in at capture time.
Poke bytes into memory starting at addr.
Parameters
-
addr(int): 16-bit BBC address (0-65535). -
data(bytes): Bytes to write.
Returns
None.
Errors
ValueErrorifaddris out of range or the write would exceed the 6502 address space.
Example
bbc.writeMemory(0x7C00, b"HELLO" + b" " * 35)Notes
Writes longer than 16 bytes are chunked internally into 16-byte writem commands to keep command lines manageable. The byte content round-trips through readMemory without translation.
Return the bounded ring of beebjit's stderr as a decoded string.
Returns
str. The ring is capped at 16 KB, so the return value is safe to log even after a long session. Most useful for post-mortem after a BeebjitError.
Example
try:
bbc.loadDisc(0, "/path/that/does/not/exist.ssd")
except BeebjitError as exc:
print("disc load failed:", exc)
print("stderr tail:", bbc.stderrTail())Send one raw debugger command and return its output lines. The escape hatch for anything the driver does not yet wrap.
Parameters
-
cmd(str): Debugger command exactly as beebjit'sdebug.caccepts it. -
timeout(float, default5.0): Host-side timeout on the prompt round-trip. Long-running commands (likecover many millions of cycles) need a much larger value, which is whyrunCyclespasses its own.
Returns
list[str]. Output lines between the command echo and the next prompt, with the prompt stripped. Parsing is the caller's responsibility.
Errors
-
BeebjitErrorif beebjit's stdin is closed, or if the subprocess exits during the command. The error message includes the stderr tail. -
TimeoutErrorif no prompt arrives withintimeout.
Example
lines = bbc.sendCommand("crtc")
for line in lines:
print(line)Notes
Useful for any debugger command beebjit supports that the driver does not yet wrap: bm, bmr, bmw, bl, find, sys, crtc, vias, eval, and similar. See debug.c lines 2165-2550 in the beebjit source for the full command grammar.
The driver itself is built on top of sendCommand; nothing else has a hotline to the subprocess.
BeebjitError is the single exception class for anything the driver cannot complete. Pipe-level failures (broken pipe, unexpected EOF), protocol-level failures (unparseable register or memory output), and command-level failures (the debugger rejecting an argument) all surface as BeebjitError. The message always includes the stderr tail when available, so a caught exception carries enough context for post-mortem on its own.
from beebjit_mcp import BeebjitError
try:
bbc.loadDisc(0, "/path/that/does/not/exist.ssd")
except BeebjitError as exc:
print("disc load failed:", exc)
print("stderr tail:", bbc.stderrTail())Argument validation errors raise ValueError instead, before any debugger round trip. Examples: addr out of the 6502 range, length negative, drive not 0 or 1, mutable=True without writeable=True.
UnsupportedCharError (a ValueError subclass, from beebjit_mcp.keyboard) is raised when typeText or typeTextRaw is asked to type a character that has no BBC key mapping.
TimeoutError surfaces from start, runCycles, and sendCommand when the prompt does not arrive within the requested timeout.
One driver instance owns one beebjit subprocess. The driver runs three threads internally (main, stdout reader, stderr reader), so callers do not see threading directly. From the outside, the contract is straightforward:
-
One operation at a time per driver. The class is not thread-safe for concurrent calls. For overlapping work, use separate
BeebjitDriverinstances. -
Multiple drivers in the same process are fully independent. Each holds its own subprocess; nothing is shared between them.
-
The driver does not implement
__del__. Garbage collection does not clean up the subprocess. Use the context manager, or callcloseexplicitly.
-
An abandoned driver leaks a beebjit subprocess. The
-cyclescap (one trillion by default) eventually trips and beebjit exits on its own, but that can be hours later. Usewith. -
startis required before any operation. The context manager calls it automatically. Manual users sometimes forget after__init__, which results in aBeebjitError("driver not started"). -
A subprocess that exits between commands (cycles cap hit, host kill signal) surfaces on the next call as
BeebjitError("beebjit stdin closed: ...")with the stderr tail attached.
Three pure helpers live in beebjit_mcp.keyboard. They run without a driver, so they are useful for offline keymap testing, custom input flows, and any caller that wants to inspect the translation before sending it.
Translate an ASCII string to a list of (bbcKey, shifted) tuples, assuming CAPS LOCK ON.
Parameters
text(str): ASCII string to translate.\nbecomes a(BBC_KEY_RETURN, False)tuple.
Returns
list[tuple[int, bool]]. Each tuple is one key-tap; the second field indicates whether the caller should hold SHIFT across the press.
Errors
UnsupportedCharErrorfor characters with no BBC matrix mapping.
Example
from beebjit_mcp.keyboard import asciiToKeys
for key, shifted in asciiToKeys('PRINT "HI"\n'):
print(key, shifted)Notes
Under CAPS LOCK ON, uppercase and lowercase letters both translate to the same unshifted matrix tap. The translation is deterministic and pure: no driver, no subprocess.
Case-preserving variant of asciiToKeys. Assumes CAPS LOCK OFF.
Parameters
text(str): ASCII string to translate.
Returns
list[tuple[int, bool]].
Errors
UnsupportedCharErrorfor characters with no BBC matrix mapping.
Example
from beebjit_mcp.keyboard import asciiToKeysRaw
asciiToKeysRaw("Hi") # [(72, True), (73, False)] -> SHIFT+H, then iNotes
Lowercase letters translate to unshifted taps; uppercase letters translate to shifted taps. The matrix key itself is always the uppercase ASCII code.
Resolve a symbolic key name to a BBC scancode. Passes integer key codes through unchanged after a range check.
Parameters
name(str | int): Symbolic name ("return","escape","caps_lock","f0"-"f9","up_arrow", etc., case-insensitive) or a raw integer 0-255.
Returns
int. The BBC scancode.
Errors
ValueErrorif the name is not inSPECIAL_KEYS, or if an integer is outside 0-255.
Example
from beebjit_mcp.keyboard import resolveKeyName
resolveKeyName("RETURN") # 131
resolveKeyName("caps_lock") # 135
resolveKeyName(65) # 65Notes
Designed for callers that accept either a friendly name or a raw code in the same parameter slot, such as the MCP key_down and key_up tools.
Three pure helpers live in beebjit_mcp.screen. They operate on byte buffers, not a live driver, so any 1000-byte MODE 7 framebuffer from any source can be decoded with them.
Decode a 1000-byte MODE 7 framebuffer into 25 rows of 40 characters.
Parameters
-
framebuffer(bytes): ExactlyMODE7_BYTES(1000) bytes in display order. Typically the return value ofcaptureMode7Bytes. -
controls(Mode7Controls, defaultMode7Controls.SPACE): How to render non-printing teletext control bytes. One of:Mode7Controls.SPACE: single space per non-printable byte. Rows stay 40 chars wide.Mode7Controls.QUESTION: single?per non-printable byte. Rows stay 40 chars wide.Mode7Controls.ESCAPE: four-character\xNNescape per non-printable byte. Row widths become variable.
Returns
list[str]. Always exactly 25 strings.
Errors
ValueErrorifframebufferis not exactly 1000 bytes.
Example
from beebjit_mcp.screen import decodeMode7, Mode7Controls
rows = decodeMode7(bbc.captureMode7Bytes(), controls=Mode7Controls.ESCAPE)Notes
Rows are not right-stripped, so column-indexing into the SPACE or QUESTION outputs stays straightforward. Use SPACE for substring assertions and column-indexing callers; ESCAPE when the original byte values matter (debugging control-code sequences, for example).
Rotate a 1024-byte MODE 7 page into display order.
Parameters
-
page(bytes): Exactly 1024 bytes of the raw teletext page (&7C00-&7FFF). -
startAddr(int): CPU-space pointer to the first displayed byte, as the MOS keeps it at&0350/&0351. Must be in&7C00-&7FFF.
Returns
bytes. Exactly 1000 bytes in display order, ready for decodeMode7.
Errors
ValueErrorifpageis not 1024 bytes, orstartAddris outside the MODE 7 page range.
Example
from beebjit_mcp.screen import rotateMode7Page
page = bbc.readMemory(0x7C00, 1024)
ptr = bbc.readMemory(0x0350, 2)
startAddr = ptr[0] | (ptr[1] << 8)
display = rotateMode7Page(page, startAddr)Notes
Called internally by captureMode7Bytes. Exposed for callers who already have a teletext page from elsewhere (a save state, a memory dump, a different emulator's frame).
True if needle appears anywhere in the decoded screen. Rows are \n-joined so a needle does not accidentally span a row boundary.
Parameters
-
framebuffer(bytes): Exactly 1000 bytes in display order. -
needle(str): Substring to search for in the joined rows.
Returns
bool. True if needle is found, False otherwise.
Errors
ValueErrorpropagated fromdecodeMode7ifframebufferis the wrong length.
Example
from beebjit_mcp.screen import mode7TextContains
while not mode7TextContains(bbc.captureMode7Bytes(), "READY"):
bbc.runCycles(500_000)Notes
The building block for "run until text appears" loops in Python, mirroring the MCP server's run_until_text tool. Use \n-aware needles when you need to assert on multi-row sequences.
One pure helper lives in beebjit_mcp.image. It takes raw pixel bytes and returns PNG bytes.
Encode raw BGRA pixel data as a PNG.
Parameters
-
bgra(bytes): BGRA pixel data, exactlywidth * height * 4bytes. Typically the first element of acaptureScreenreturn value. -
width(int): Image width in pixels. -
height(int): Image height in pixels.
Returns
bytes. A complete PNG file, including the 8-byte signature and the IEND chunk. Ready to write to disk, base64-encode, or hand to any PNG viewer.
Errors
ValueErroriflen(bgra) != width * height * 4, or ifwidthorheightis non-positive.
Example
from beebjit_mcp.image import bgraToPng
bgra, width, height = bbc.captureScreen()
png = bgraToPng(bgra, width, height)
Path("screen.png").write_bytes(png)Notes
The MCP screenshot tool composes captureScreen and bgraToPng internally and returns the result as an image content block. Library callers who want the same end product can call both directly and skip the MCP wrapping.
-
Tool reference is the per-tool MCP detail. Many tools map one-to-one with a Python method; some (
run_until_text,set_caps_lock,read_mode7_text,screenshot,run_basic) are server-side compositions, and the Python equivalent is shown in the relevant section above. -
Architecture covers how the driver, keyboard, screen, and rendering pieces fit together inside the package.
-
Keypress timing is the reasoning behind the five-million-cycle HOLD and GAP defaults.
-
MODE 7 decode covers teletext control codes and the hardware-scroll pointer.
-
Troubleshooting covers common failure modes; everything there applies here as well.
