Skip to main content
Glama
simen

VICE C64 Emulator MCP Server

by simen

readScreen

Extract displayed text from Commodore 64 screen memory by converting PETSCII codes to readable ASCII. Get 25 lines of 40 characters to view program output during debugging.

Instructions

Read the C64 screen memory and return it as interpreted text.

Converts PETSCII screen codes to readable ASCII. Returns 25 lines of 40 characters.

Use this instead of readMemory($0400) when you want to see what's displayed on screen.

Note: This reads from the current screen RAM location (may not be $0400 if the program moved it). In bitmap modes, the data won't represent text.

Options:

  • format: "full" (default) returns all 25 lines, "summary" returns only non-empty lines

  • includeRaw: Also return raw screen codes (default: false)

Related tools: readColorRam, readVicState, readMemory

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
formatNoOutput format: 'full' (all 25 lines) or 'summary' (non-empty lines only)
includeRawNoInclude raw screen codes array (default: false)

Implementation Reference

  • The core handler function for the readScreen tool. Determines current VIC-II bank and screen memory address by reading CIA2 ($DD00) and VIC $D018 registers. Reads 1000 bytes of screen RAM. Converts PETSCII screen codes to readable ASCII text using screenToText helper. Handles full/summary formats, includes raw data option, detects graphics mode, and adds contextual hints.
    async (args) => {
      try {
        // First get VIC bank from CIA2
        const cia2Data = await client.readMemory(0xdd00, 0xdd00);
        const bankInfo = getVicBank(cia2Data[0]);
    
        // Get screen address from $D018
        const d018Data = await client.readMemory(0xd018, 0xd018);
        const videoAddrs = getVideoAddresses(d018Data[0], bankInfo.baseAddress);
    
        // Read screen RAM (1000 bytes)
        const screenData = await client.readMemory(
          videoAddrs.screenAddress,
          videoAddrs.screenAddress + 999
        );
    
        // Convert to text
        const textLines = screenToText(screenData);
    
        // Find non-empty lines for summary
        const nonEmptyLines = textLines
          .map((line, idx) => ({ line: idx, content: line }))
          .filter((l) => l.content.trim().length > 0);
    
        const useSummaryFormat = args.format === "summary";
    
        const response: Record<string, unknown> = {
          screenAddress: {
            value: videoAddrs.screenAddress,
            hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`,
          },
          vicBank: bankInfo.bank,
          format: useSummaryFormat ? "summary" : "full",
        };
    
        if (useSummaryFormat) {
          // Summary format: only non-empty lines with line numbers
          response.lines = nonEmptyLines.map((l) => ({
            lineNumber: l.line,
            content: l.content,
          }));
          response.totalLines = 25;
          response.nonEmptyCount = nonEmptyLines.length;
        } else {
          // Full format: all 25 lines
          response.lines = textLines;
          response.summary = {
            nonEmptyLines: nonEmptyLines.length,
            preview:
              nonEmptyLines.length > 0
                ? nonEmptyLines.slice(0, 3).map((l) => `Line ${l.line}: "${l.content}"`)
                : ["Screen appears empty"],
          };
        }
    
        if (args.includeRaw) {
          response.raw = Array.from(screenData);
        }
    
        // Check graphics mode and add hint
        const d011Data = await client.readMemory(0xd011, 0xd011);
        const d016Data = await client.readMemory(0xd016, 0xd016);
        const graphicsMode = getGraphicsMode(d011Data[0], d016Data[0]);
    
        response.graphicsMode = graphicsMode.mode;
    
        if (graphicsMode.bitmap) {
          response.hint =
            "Warning: VIC-II is in bitmap mode - screen RAM contains bitmap data, not text.";
        } else if (nonEmptyLines.length === 0) {
          response.hint = "Screen appears empty or contains only spaces.";
        } else {
          response.hint = `Screen has ${nonEmptyLines.length} non-empty line(s). First: "${nonEmptyLines[0]?.content || ""}"`;
        }
    
        return formatResponse(response);
      } catch (error) {
        return formatError(error as ViceError);
      }
    }
  • Zod input schema defining parameters for readScreen: format (full/summary) and includeRaw (boolean).
    inputSchema: z.object({
      format: z
        .enum(["full", "summary"])
        .optional()
        .describe("Output format: 'full' (all 25 lines) or 'summary' (non-empty lines only)"),
      includeRaw: z
        .boolean()
        .optional()
        .describe("Include raw screen codes array (default: false)"),
    }),
  • src/index.ts:1053-1161 (registration)
    MCP server tool registration for readScreen, including detailed description, input schema validation, and handler function reference.
    server.registerTool(
      "readScreen",
      {
        description: `Read the C64 screen memory and return it as interpreted text.
    
    Converts PETSCII screen codes to readable ASCII. Returns 25 lines of 40 characters.
    
    Use this instead of readMemory($0400) when you want to see what's displayed on screen.
    
    Note: This reads from the current screen RAM location (may not be $0400 if the program moved it).
    In bitmap modes, the data won't represent text.
    
    Options:
    - format: "full" (default) returns all 25 lines, "summary" returns only non-empty lines
    - includeRaw: Also return raw screen codes (default: false)
    
    Related tools: readColorRam, readVicState, readMemory`,
        inputSchema: z.object({
          format: z
            .enum(["full", "summary"])
            .optional()
            .describe("Output format: 'full' (all 25 lines) or 'summary' (non-empty lines only)"),
          includeRaw: z
            .boolean()
            .optional()
            .describe("Include raw screen codes array (default: false)"),
        }),
      },
      async (args) => {
        try {
          // First get VIC bank from CIA2
          const cia2Data = await client.readMemory(0xdd00, 0xdd00);
          const bankInfo = getVicBank(cia2Data[0]);
    
          // Get screen address from $D018
          const d018Data = await client.readMemory(0xd018, 0xd018);
          const videoAddrs = getVideoAddresses(d018Data[0], bankInfo.baseAddress);
    
          // Read screen RAM (1000 bytes)
          const screenData = await client.readMemory(
            videoAddrs.screenAddress,
            videoAddrs.screenAddress + 999
          );
    
          // Convert to text
          const textLines = screenToText(screenData);
    
          // Find non-empty lines for summary
          const nonEmptyLines = textLines
            .map((line, idx) => ({ line: idx, content: line }))
            .filter((l) => l.content.trim().length > 0);
    
          const useSummaryFormat = args.format === "summary";
    
          const response: Record<string, unknown> = {
            screenAddress: {
              value: videoAddrs.screenAddress,
              hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`,
            },
            vicBank: bankInfo.bank,
            format: useSummaryFormat ? "summary" : "full",
          };
    
          if (useSummaryFormat) {
            // Summary format: only non-empty lines with line numbers
            response.lines = nonEmptyLines.map((l) => ({
              lineNumber: l.line,
              content: l.content,
            }));
            response.totalLines = 25;
            response.nonEmptyCount = nonEmptyLines.length;
          } else {
            // Full format: all 25 lines
            response.lines = textLines;
            response.summary = {
              nonEmptyLines: nonEmptyLines.length,
              preview:
                nonEmptyLines.length > 0
                  ? nonEmptyLines.slice(0, 3).map((l) => `Line ${l.line}: "${l.content}"`)
                  : ["Screen appears empty"],
            };
          }
    
          if (args.includeRaw) {
            response.raw = Array.from(screenData);
          }
    
          // Check graphics mode and add hint
          const d011Data = await client.readMemory(0xd011, 0xd011);
          const d016Data = await client.readMemory(0xd016, 0xd016);
          const graphicsMode = getGraphicsMode(d011Data[0], d016Data[0]);
    
          response.graphicsMode = graphicsMode.mode;
    
          if (graphicsMode.bitmap) {
            response.hint =
              "Warning: VIC-II is in bitmap mode - screen RAM contains bitmap data, not text.";
          } else if (nonEmptyLines.length === 0) {
            response.hint = "Screen appears empty or contains only spaces.";
          } else {
            response.hint = `Screen has ${nonEmptyLines.length} non-empty line(s). First: "${nonEmptyLines[0]?.content || ""}"`;
          }
    
          return formatResponse(response);
        } catch (error) {
          return formatError(error as ViceError);
        }
      }
    );
  • Helper function that converts raw screen RAM buffer (1000 bytes) into 25 lines x 40 chars of ASCII text by mapping C64 screen codes to printable characters via screenCodeToAscii.
    export function screenToText(screenData: Buffer | number[]): string[] {
      const data = Buffer.isBuffer(screenData) ? screenData : Buffer.from(screenData);
      const lines: string[] = [];
    
      for (let row = 0; row < 25; row++) {
        const offset = row * 40;
        let line = "";
        for (let col = 0; col < 40; col++) {
          if (offset + col < data.length) {
            line += screenCodeToAscii(data[offset + col]);
          }
        }
        // Trim trailing spaces but keep the line
        lines.push(line.trimEnd());
      }
    
      return lines;
    }
  • Calculates VIC-II memory bank (0-3) and base address ($0000, $4000, $8000, $C000) from CIA #2 port A register value ($DD00). Essential for locating screen RAM.
    export function getVicBank(cia2PortA: number): { bank: number; baseAddress: number } {
      // CIA2 port A bits 0-1 (inverted) select the bank
      const bankBits = (~cia2PortA) & 0x03;
      return {
        bank: bankBits,
        baseAddress: bankBits * 0x4000,
      };
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does well by disclosing key behavioral traits: it explains the output format (25 lines of 40 characters), notes technical details (screen RAM location may vary, bitmap mode limitations), and describes optional parameters. However, it doesn't cover aspects like error handling or performance.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded with the core purpose, followed by usage guidance, notes, and parameter details. Every sentence adds value without redundancy, making it efficient and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity, no annotations, and no output schema, the description is largely complete: it covers purpose, usage, behavioral notes, and parameters. However, it could benefit from more detail on output structure or error cases to be fully comprehensive.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already fully documents both parameters. The description adds minimal value by listing the options with brief explanations, but doesn't provide additional semantics beyond what's in the schema, warranting the baseline score of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Read the C64 screen memory'), resource ('screen memory'), and transformation ('converts PETSCII screen codes to readable ASCII'), distinguishing it from siblings like readMemory by specifying its specialized purpose for screen display interpretation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states when to use this tool ('Use this instead of readMemory($0400) when you want to see what's displayed on screen') and provides context on limitations ('In bitmap modes, the data won't represent text'), with related tools listed for alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/simen/vice-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server