Skip to main content
Glama
simen

VICE C64 Emulator MCP Server

by simen

disassemble

Convert 6502 machine code from a Commodore 64 memory address into readable assembly instructions with labels and branch targets for debugging.

Instructions

Disassemble 6502 machine code at a memory address.

Returns human-readable assembly instructions with:

  • Address and raw bytes

  • Mnemonic and operand

  • Branch target addresses (for branch instructions)

  • Known KERNAL/BASIC labels

Options:

  • address: Start address (default: current PC)

  • count: Number of instructions (default: 10)

Related tools: readMemory, getRegisters, step

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
addressNoStart address (default: current PC)
countNoNumber of instructions to disassemble (default: 10)

Implementation Reference

  • The main handler function for the 'disassemble' tool. It optionally gets current PC, reads memory from VICE, calls the disassemble helper, adds labels, formats the assembly listing, and returns structured response.
    async (args) => {
      try {
        // Get PC if no address specified
        let startAddress = args.address;
        if (startAddress === undefined) {
          const regResponse = await client.getRegisters();
          const count = regResponse.body.readUInt16LE(0);
          let offset = 2;
          for (let i = 0; i < count && offset < regResponse.body.length; i++) {
            const id = regResponse.body[offset];
            const size = regResponse.body[offset + 1];
            offset += 2;
            if (id === 3 && size === 2) {
              startAddress = regResponse.body.readUInt16LE(offset);
              break;
            }
            offset += size;
          }
          startAddress = startAddress ?? 0;
        }
    
        const instructionCount = args.count || 10;
    
        // Read enough bytes (max 3 bytes per instruction)
        const bytesToRead = Math.min(instructionCount * 3, 0x10000 - startAddress);
        const endAddress = Math.min(startAddress + bytesToRead - 1, 0xffff);
        const memData = await client.readMemory(startAddress, endAddress);
    
        // Disassemble
        const instructions = disassemble(memData, startAddress, instructionCount);
    
        // Add labels for known addresses
        const instructionsWithLabels = instructions.map((instr) => {
          const label = getLabelForAddress(instr.address);
          const targetLabel = instr.branchTarget ? getLabelForAddress(instr.branchTarget) : undefined;
          return {
            ...instr,
            label,
            targetLabel,
          };
        });
    
        // Format for output
        const lines = instructionsWithLabels.map((instr) => {
          let line = `${instr.addressHex}: ${instr.bytesHex.padEnd(8)} ${instr.fullInstruction}`;
          if (instr.label) line += `  ; ${instr.label}`;
          if (instr.targetLabel) line += `  ; -> ${instr.targetLabel}`;
          return line;
        });
    
        return formatResponse({
          startAddress: {
            value: startAddress,
            hex: `$${startAddress.toString(16).padStart(4, "0")}`,
          },
          instructionCount: instructions.length,
          instructions: instructionsWithLabels,
          listing: lines.join("\n"),
          hint:
            instructions.length > 0 && instructions[0].mnemonic === "BRK"
              ? "First instruction is BRK - this might be uninitialized memory or data"
              : `Disassembled ${instructions.length} instruction(s) from $${startAddress.toString(16).padStart(4, "0")}`,
        });
      } catch (error) {
        return formatError(error as ViceError);
      }
    }
  • Zod input schema defining optional 'address' (0-FFFF, defaults to PC) and 'count' (1-100, default 10) parameters for disassembly.
      inputSchema: z.object({
        address: z
          .number()
          .min(0)
          .max(0xffff)
          .optional()
          .describe("Start address (default: current PC)"),
        count: z.number().min(1).max(100).optional().describe("Number of instructions to disassemble (default: 10)"),
      }),
    },
  • src/index.ts:848-941 (registration)
    MCP tool registration call for 'disassemble', specifying name, description, input schema, and handler function.
    server.registerTool(
      "disassemble",
      {
        description: `Disassemble 6502 machine code at a memory address.
    
    Returns human-readable assembly instructions with:
    - Address and raw bytes
    - Mnemonic and operand
    - Branch target addresses (for branch instructions)
    - Known KERNAL/BASIC labels
    
    Options:
    - address: Start address (default: current PC)
    - count: Number of instructions (default: 10)
    
    Related tools: readMemory, getRegisters, step`,
        inputSchema: z.object({
          address: z
            .number()
            .min(0)
            .max(0xffff)
            .optional()
            .describe("Start address (default: current PC)"),
          count: z.number().min(1).max(100).optional().describe("Number of instructions to disassemble (default: 10)"),
        }),
      },
      async (args) => {
        try {
          // Get PC if no address specified
          let startAddress = args.address;
          if (startAddress === undefined) {
            const regResponse = await client.getRegisters();
            const count = regResponse.body.readUInt16LE(0);
            let offset = 2;
            for (let i = 0; i < count && offset < regResponse.body.length; i++) {
              const id = regResponse.body[offset];
              const size = regResponse.body[offset + 1];
              offset += 2;
              if (id === 3 && size === 2) {
                startAddress = regResponse.body.readUInt16LE(offset);
                break;
              }
              offset += size;
            }
            startAddress = startAddress ?? 0;
          }
    
          const instructionCount = args.count || 10;
    
          // Read enough bytes (max 3 bytes per instruction)
          const bytesToRead = Math.min(instructionCount * 3, 0x10000 - startAddress);
          const endAddress = Math.min(startAddress + bytesToRead - 1, 0xffff);
          const memData = await client.readMemory(startAddress, endAddress);
    
          // Disassemble
          const instructions = disassemble(memData, startAddress, instructionCount);
    
          // Add labels for known addresses
          const instructionsWithLabels = instructions.map((instr) => {
            const label = getLabelForAddress(instr.address);
            const targetLabel = instr.branchTarget ? getLabelForAddress(instr.branchTarget) : undefined;
            return {
              ...instr,
              label,
              targetLabel,
            };
          });
    
          // Format for output
          const lines = instructionsWithLabels.map((instr) => {
            let line = `${instr.addressHex}: ${instr.bytesHex.padEnd(8)} ${instr.fullInstruction}`;
            if (instr.label) line += `  ; ${instr.label}`;
            if (instr.targetLabel) line += `  ; -> ${instr.targetLabel}`;
            return line;
          });
    
          return formatResponse({
            startAddress: {
              value: startAddress,
              hex: `$${startAddress.toString(16).padStart(4, "0")}`,
            },
            instructionCount: instructions.length,
            instructions: instructionsWithLabels,
            listing: lines.join("\n"),
            hint:
              instructions.length > 0 && instructions[0].mnemonic === "BRK"
                ? "First instruction is BRK - this might be uninitialized memory or data"
                : `Disassembled ${instructions.length} instruction(s) from $${startAddress.toString(16).padStart(4, "0")}`,
          });
        } catch (error) {
          return formatError(error as ViceError);
        }
      }
    );
  • Core disassembly logic: parses 6502 opcodes from memory buffer starting at given address, handles all addressing modes, computes branch targets, formats assembly syntax, returns array of structured instructions.
    export function disassemble(
      data: Buffer | number[],
      startAddress: number,
      count?: number
    ): DisassembledInstruction[] {
      const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data);
      const result: DisassembledInstruction[] = [];
      let offset = 0;
      let instructionCount = 0;
    
      while (offset < bytes.length) {
        if (count !== undefined && instructionCount >= count) break;
    
        const address = startAddress + offset;
        const opcode = bytes[offset];
        const entry = OPCODES[opcode];
    
        if (!entry) {
          // Unknown opcode - treat as single byte data
          result.push({
            address,
            addressHex: `$${address.toString(16).padStart(4, "0")}`,
            bytes: [opcode],
            bytesHex: opcode.toString(16).padStart(2, "0"),
            mnemonic: "???",
            operand: `$${opcode.toString(16).padStart(2, "0")}`,
            fullInstruction: `??? $${opcode.toString(16).padStart(2, "0")}`,
            size: 1,
          });
          offset++;
          instructionCount++;
          continue;
        }
    
        const [mnemonic, mode, size] = entry;
    
        // Check if we have enough bytes
        if (offset + size > bytes.length) break;
    
        const instrBytes: number[] = [];
        for (let i = 0; i < size; i++) {
          instrBytes.push(bytes[offset + i]);
        }
    
        const bytesHex = instrBytes.map((b) => b.toString(16).padStart(2, "0")).join(" ");
        let operand = "";
        let branchTarget: number | undefined;
        let branchTargetHex: string | undefined;
    
        switch (mode) {
          case "impl":
            operand = "";
            break;
          case "acc":
            operand = "A";
            break;
          case "imm":
            operand = `#$${bytes[offset + 1].toString(16).padStart(2, "0")}`;
            break;
          case "zp":
            operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")}`;
            break;
          case "zpx":
            operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")},X`;
            break;
          case "zpy":
            operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")},Y`;
            break;
          case "abs":
            operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")}`;
            break;
          case "abx":
            operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")},X`;
            break;
          case "aby":
            operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")},Y`;
            break;
          case "ind":
            operand = `($${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")})`;
            break;
          case "izx":
            operand = `($${bytes[offset + 1].toString(16).padStart(2, "0")},X)`;
            break;
          case "izy":
            operand = `($${bytes[offset + 1].toString(16).padStart(2, "0")}),Y`;
            break;
          case "rel": {
            // Relative branch - calculate target address
            const displacement = bytes[offset + 1];
            // Convert to signed
            const signed = displacement > 127 ? displacement - 256 : displacement;
            branchTarget = (address + 2 + signed) & 0xffff;
            branchTargetHex = `$${branchTarget.toString(16).padStart(4, "0")}`;
            operand = branchTargetHex;
            break;
          }
        }
    
        const fullInstruction = operand ? `${mnemonic} ${operand}` : mnemonic;
    
        result.push({
          address,
          addressHex: `$${address.toString(16).padStart(4, "0")}`,
          bytes: instrBytes,
          bytesHex,
          mnemonic,
          operand,
          fullInstruction,
          size,
          branchTarget,
          branchTargetHex,
        });
    
        offset += size;
        instructionCount++;
      }
    
      return result;
    }
  • TypeScript interface defining the output structure for each disassembled instruction, used by the disassemble function and handler.
    export interface DisassembledInstruction {
      address: number;
      addressHex: string;
      bytes: number[];
      bytesHex: string;
      mnemonic: string;
      operand: string;
      fullInstruction: string;
      size: number;
      // For branches: the target address
      branchTarget?: number;
      branchTargetHex?: string;
    }
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 of behavioral disclosure. It effectively describes what the tool does (disassembles machine code), what it returns (human-readable assembly with specific details like addresses, bytes, mnemonics, branch targets, and labels), and default behaviors (address defaults to current PC, count defaults to 10). However, it doesn't mention potential limitations like error handling or performance constraints.

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 appropriately sized, with a clear purpose statement upfront, detailed output format, parameter options, and related tools. Every sentence adds value without redundancy, making it efficient and easy to parse for an AI agent.

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 (2 parameters, no output schema, no annotations), the description is largely complete. It covers purpose, output details, parameters, defaults, and related tools. However, without an output schema, it could benefit from more specifics on the return structure (e.g., format of the human-readable instructions), though the listed output components provide good context.

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 documents both parameters (address and count) with their descriptions, types, and constraints. The description repeats the default values and adds context about 'current PC' for address, but doesn't provide significant additional semantic meaning beyond what's in the schema. This meets the baseline for high schema coverage.

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 tool's purpose with a specific verb ('disassemble') and resource ('6502 machine code at a memory address'), distinguishing it from siblings like readMemory or getRegisters. It explicitly mentions the output format (human-readable assembly instructions) and the specific processor architecture (6502), making the purpose unambiguous and distinct.

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?

The description provides explicit usage guidance by listing related tools (readMemory, getRegisters, step) and specifying when to use this tool (for disassembling code) versus alternatives (e.g., readMemory for raw bytes, getRegisters for CPU state). It also implies context by mentioning 'current PC' as a default, suggesting it's used in debugging scenarios.

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