From b03eefc6f500c489d4a9c3d8575173287e61e7ed Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sat, 21 Jun 2025 22:19:35 +0200 Subject: [PATCH 01/17] WIP: MCP (Model Context Protocol) server experiment --- .vscode/launch.json | 6 + .vscode/mcp.json | 17 ++ .vscode/tasks.json | 16 ++ dotnet-6502.sln | 17 ++ .../C64Tool.cs | 236 ++++++++++++++++++ .../Contract/CPURegisters.cs | 48 ++++ .../Emulator/EmbeddedMCPHostApp.cs | 125 ++++++++++ .../Emulator/EmulatorConfig.cs | 38 +++ .../Emulator/SystemSetup/C64HostConfig.cs | 53 ++++ .../Emulator/SystemSetup/C64Setup.cs | 79 ++++++ .../Highbyte.DotNet6502.Util.MCPServer.csproj | 31 +++ .../Program.cs | 69 +++++ .../appsettings.json | 29 +++ 13 files changed, 764 insertions(+) create mode 100644 .vscode/mcp.json create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/CPURegisters.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmulatorConfig.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64HostConfig.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64Setup.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/appsettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json index f34fa11a5..74e6daa47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -56,6 +56,12 @@ "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" + }, + { + "name": ".NET Core Attach MCPServer", + "type": "coreclr", + "request": "attach", + "processName": "Highbyte.DotNet6502.Util.MCPServer.exe" } ] } \ No newline at end of file diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..b5900ca51 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,17 @@ +{ + "inputs": [], + "servers": { + "DotNet6502": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj", + "--configuration", + "Debug" + ], + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" + } + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 357d7e327..4a6d34e2e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -116,6 +116,22 @@ "kind": "test", "isDefault": true } + }, + { + "label": "build mcp server", + "command": "dotnet", + "type": "attach", + "args": [ + "build", + "${workspaceFolder}/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } } ] } \ No newline at end of file diff --git a/dotnet-6502.sln b/dotnet-6502.sln index d60ed7718..eddfdf377 100644 --- a/dotnet-6502.sln +++ b/dotnet-6502.sln @@ -47,6 +47,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Highbyte.DotNet6502.Systems EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Highbyte.DotNet6502.AI", "src\libraries\Highbyte.DotNet6502.AI\Highbyte.DotNet6502.AI.csproj", "{B04E0DA2-F0EB-4E71-BFF9-3D3B17E30688}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Highbyte.DotNet6502.Util.MCPServer", "src\utils\Highbyte.DotNet6502.Util.MCPServer\Highbyte.DotNet6502.Util.MCPServer.csproj", "{1AB6BB50-42BF-44ED-8B5B-40266C9ED016}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -273,6 +277,18 @@ Global {B04E0DA2-F0EB-4E71-BFF9-3D3B17E30688}.Release|x64.Build.0 = Release|Any CPU {B04E0DA2-F0EB-4E71-BFF9-3D3B17E30688}.Release|x86.ActiveCfg = Release|Any CPU {B04E0DA2-F0EB-4E71-BFF9-3D3B17E30688}.Release|x86.Build.0 = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|x64.Build.0 = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Debug|x86.Build.0 = Debug|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|Any CPU.Build.0 = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|x64.ActiveCfg = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|x64.Build.0 = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|x86.ActiveCfg = Release|Any CPU + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -296,6 +312,7 @@ Global {4E078F4C-9C1B-4C17-A2AE-6BF2345DC791} = {B406AD5D-CB8D-45F2-A5A2-4C0AD82A410A} {A52EFEC2-FD60-453E-AC55-703AD2F2A895} = {B406AD5D-CB8D-45F2-A5A2-4C0AD82A410A} {B04E0DA2-F0EB-4E71-BFF9-3D3B17E30688} = {B406AD5D-CB8D-45F2-A5A2-4C0AD82A410A} + {1AB6BB50-42BF-44ED-8B5B-40266C9ED016} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0F55B6C2-E4B4-4F2C-9D2A-D63A17F3B5C4} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs new file mode 100644 index 000000000..d1a4f239e --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs @@ -0,0 +1,236 @@ +using System.ComponentModel; +using Highbyte.DotNet6502; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Util.MCPServer.Contract; +using Highbyte.DotNet6502.Util.MCPServer.Emulator; +using Highbyte.DotNet6502.Utils; +using ModelContextProtocol.Server; + +[McpServerToolType] +public static class C64Tool +{ + [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] + public static EmulatorState GetState(EmbeddedMCPHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + return hostApp.EmulatorState; + } + + [McpServerTool, Description("Starts C64 emulator.")] + public static async Task Start(EmbeddedMCPHostApp hostApp) + { + AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + await hostApp.Start(); + } + + [McpServerTool, Description("Pause C64 emulator")] + public static async Task Pause(EmbeddedMCPHostApp hostApp) + { + AssertC64EmulatorIsRunning(hostApp); + hostApp.Pause(); + } + + [McpServerTool, Description("Stop C64 emulator")] + public static async Task Stop(EmbeddedMCPHostApp hostApp) + { + AssertC64EmulatorIsRunningOrPaused(hostApp); + hostApp.Stop(); + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] + public static async Task RunNumberOfSeconds(EmbeddedMCPHostApp hostApp, int numberOfSeconds) + { + AssertC64EmulatorIsRunning(hostApp); + + if (numberOfSeconds <= 0) + throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); + + var c64 = GetC64(hostApp); + //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? + int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); + await RunNumberOfFrames(hostApp, numberOfFrames); + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] + public static async Task RunNumberOfFrames(EmbeddedMCPHostApp hostApp, int numberOfFrames) + { + AssertC64EmulatorIsRunning(hostApp); + + if (numberOfFrames <= 0) + throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); + for (int i = 0; i < numberOfFrames; i++) + hostApp.RunEmulatorOneFrame(); + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] + public static async Task RunNumberOfInstructions(EmbeddedMCPHostApp hostApp, int numberOfInstructions) + { + AssertC64EmulatorIsRunning(hostApp); + + if (numberOfInstructions <= 0) + throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); + for (int i = 0; i < numberOfInstructions; i++) + hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + } + + [McpServerTool, Description("Returns value of specified memory address in C64 emulator")] + public static byte ReadMemory(EmbeddedMCPHostApp hostApp, ushort address) + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + var value = c64.Mem[address]; + return value; + } + + [McpServerTool, Description("Returns array of values of from memory start address up to specified length in C64 emulator")] + public static byte[] ReadMemoryRange(EmbeddedMCPHostApp hostApp, ushort startAddress, ushort length) + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + + if (length <= 0) + throw new ArgumentException("Length must be greater than zero.", nameof(length)); + if (startAddress + length > c64.Mem.Size) + length = (ushort)(c64.Mem.Size - startAddress); + + var values = c64.Mem.ReadData(startAddress, length); + return values; + } + + [McpServerTool, Description("Sets value at specified memory address in C64 emulator")] + public static void WriteMemory(EmbeddedMCPHostApp hostApp, ushort address, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + c64.Mem[address] = value; + } + + [McpServerTool, Description("Sets array of values starting at specified memory address in C64 emulator. Use this if multiple values need to be set in sequence.")] + public static void WriteMemory(EmbeddedMCPHostApp hostApp, ushort address, byte[] values) + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + if (address + values.Length > c64.Mem.Size) + values = values.Take(c64.Mem.Size - address).ToArray(); + c64.Mem.StoreData(address, values); + } + + [McpServerTool, Description("Returns the current value of the C64 CPU registers: A, X, Y, PS, PC, SP")] + public static CPURegisters GetCPURegisters(EmbeddedMCPHostApp hostApp) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + + return new CPURegisters + { + A = cpu.A, + X = cpu.X, + Y = cpu.Y, + PC = cpu.PC, + SP = cpu.SP, + ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) + }; + } + + [McpServerTool, Description("Sets the CPU register A")] + public static void SetCPURegisterA(EmbeddedMCPHostApp hostApp, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.A = value; + } + + [McpServerTool, Description("Sets the CPU register X")] + public static void SetCPURegisterX(EmbeddedMCPHostApp hostApp, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.X = value; + } + + [McpServerTool, Description("Sets the CPU register Y")] + public static void SetCPURegisterY(EmbeddedMCPHostApp hostApp, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.Y = value; + } + + [McpServerTool, Description("Sets the CPU register PC (Program Counter)")] + public static void SetCPURegisterPC(EmbeddedMCPHostApp hostApp, ushort value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.PC = value; + } + + [McpServerTool, Description("Sets the CPU register SP (Stack Pointer)")] + public static void SetCPURegisterSP(EmbeddedMCPHostApp hostApp, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.SP = value; + } + + [McpServerTool, Description("Sets the CPU register PS (Processor Status)")] + public static void SetCPURegisterPS(EmbeddedMCPHostApp hostApp, byte value) + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.ProcessorStatus.Value = value; + } + + private static void AssertC64EmulatorIsRunning(EmbeddedMCPHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Running) + { + throw new InvalidOperationException($"C64 emulator is not running. Current state: {hostApp.EmulatorState}"); + } + } + + private static void AssertC64EmulatorIsRunningOrPaused(EmbeddedMCPHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Running && hostApp.EmulatorState != EmulatorState.Paused) + { + throw new InvalidOperationException($"C64 emulator is not running or paused. Current state: {hostApp.EmulatorState}"); + } + } + + private static void AssertC64EmulatorIsPausedOrUninitialzied(EmbeddedMCPHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Paused && hostApp.EmulatorState != EmulatorState.Uninitialized) + { + throw new InvalidOperationException($"C64 emulator not paused uninitialzied. Current state: {hostApp.EmulatorState}"); + } + } + + private static void AssertC64EmulatorIsUninitialzied(EmbeddedMCPHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Uninitialized) + { + throw new InvalidOperationException($"C64 emulator is running or paused. Current state: {hostApp.EmulatorState}"); + } + } + + private static void AssertEmulatorIsC64(EmbeddedMCPHostApp hostApp) + { + if (hostApp.SelectedSystemName != C64.SystemName) + { + throw new InvalidOperationException("Current emulated system is not a C64 instance."); + } + } + + private static C64 GetC64(EmbeddedMCPHostApp hostApp) + { + if (hostApp.CurrentRunningSystem is C64 c64) + { + return c64; + } + throw new InvalidOperationException("Current running system is not a C64 instance."); + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/CPURegisters.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/CPURegisters.cs new file mode 100644 index 000000000..86ccedae2 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/CPURegisters.cs @@ -0,0 +1,48 @@ + +namespace Highbyte.DotNet6502.Util.MCPServer.Contract; +public class CPURegisters +{ + /// + /// Program Counter + /// + public ushort PC { get; set; } + + /// + /// Stack Pointer + /// The 6502 microprocessor supports a 256 byte stack fixed between memory locations $0100 and $01FF. + /// A special 8-bit register, S, is used to keep track of the next free byte of stack space. + /// Pushing a byte on to the stack causes the value to be stored at the current free location (e.g. $0100,S) + /// and then the stack pointer is post decremented. + /// Pull operations reverse this procedure. + /// + /// The stack register can only be accessed by transferring its value to or from the X register via instructions TSX and TXS. + /// Its value is automatically modified by push/pull instructions, subroutine calls and returns, interrupts and returns from interrupts. + /// + /// Other instructions for storing values on stack: PHA, PHP, PLA, PLP + /// + public byte SP { get; set; } + + /// + /// Accumulator + /// + public byte A { get; set; } + + /// + /// Index Register X + /// + public byte X { get; set; } + + /// + /// Index Register Y + /// + public byte Y { get; set; } + + /// + /// Processor Status. + /// + /// As instructions are executed a set of processor flags are set or clear to record the results of the operation. This flags and some additional control flags are held in a special status register. Each flag has a single bit within the register. + /// + /// Instructions exist to test the values of the various bits, to set or clear some of them and to push or pull the entire set to or from the stack. + /// + public ProcessorStatus ProcessorStatus { get; set; } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs new file mode 100644 index 000000000..ee9244145 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs @@ -0,0 +1,125 @@ +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Logging.InMem; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.Util.MCPServer.Emulator; + +/// +/// Host app for running Highbyte.DotNet6502 emulator embbedded in a MCP server +/// +public class EmbeddedMCPHostApp : HostApp +{ + // -------------------- + // Injected variables + // -------------------- + private readonly ILogger _logger; + + private readonly DotNet6502InMemLogStore _logStore; + private readonly DotNet6502InMemLoggerConfiguration _logConfig; + private readonly IConfiguration _configuration; + private readonly ILoggerFactory _loggerFactory; + + // -------------------- + // Other variables / constants + // -------------------- + private NullRenderContext _renderContext = default!; + private NullInputHandlerContext _inputHandlerContext = default!; + private NullAudioHandlerContext _audioHandlerContext = default!; + + + private const int STATS_UPDATE_EVERY_X_FRAME = 60 * 1; + private const int DEBUGINFO_UPDATE_EVERY_X_FRAME = 10 * 1; + + private int _statsFrameCount = 0; + private int _debugInfoFrameCount = 0; + + private const int LOGS_UPDATE_EVERY_X_FRAME = 60 * 1; + private int _logsFrameCount = 0; + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + public EmbeddedMCPHostApp( + SystemList systemList, + ILoggerFactory loggerFactory, + EmulatorConfig emulatorConfig, + DotNet6502InMemLogStore logStore, + DotNet6502InMemLoggerConfiguration logConfig, + IConfiguration configuration) + : base("MCPServer", systemList, loggerFactory) + { + _logStore = logStore; + _logConfig = logConfig; + _configuration = configuration; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(typeof(EmbeddedMCPHostApp).Name); + } + + public void Init() + { + _renderContext = new NullRenderContext(); + _inputHandlerContext = new NullInputHandlerContext(); + _audioHandlerContext = new NullAudioHandlerContext(); + + SetContexts(() => _renderContext, () => _inputHandlerContext, () => _audioHandlerContext); + InitRenderContext(); + InitInputHandlerContext(); + InitAudioHandlerContext(); + } + + public override void OnAfterSelectSystem() + { + } + + public override bool OnBeforeStart(ISystem systemAboutToBeStarted) + { + return true; + } + + public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) + { + } + + public override void OnBeforeStop() + { + } + + public override void OnAfterStop() + { + } + + public override void OnAfterClose() + { + } + + + public override void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput) + { + shouldRun = true; + shouldReceiveInput = false; + } + + public override void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + } + + public override void OnBeforeDrawFrame(bool emulatorWillBeRendered) + { + // If any ImGui window is visible, make sure to clear Gl buffer before rendering emulator + if (emulatorWillBeRendered) + { + } + } + public override void OnAfterDrawFrame(bool emulatorRendered) + { + if (emulatorRendered) + { + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmulatorConfig.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmulatorConfig.cs new file mode 100644 index 000000000..a93f4b38a --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmulatorConfig.cs @@ -0,0 +1,38 @@ +using Highbyte.DotNet6502.Monitor; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Util.MCPServer.Emulator.SystemSetup; + +namespace Highbyte.DotNet6502.Util.MCPServer.Emulator; + +public class EmulatorConfig +{ + public const string ConfigSectionName = "Highbyte.DotNet6502.MCPServer"; + + /// + /// The name of the emulator to start. + /// Ex: GenericComputer, C64 + /// + /// + public string DefaultEmulator { get; set; } + + public MonitorConfig Monitor { get; set; } + + + /// + /// SadConsole-specific configuration for specific system. + /// + public C64HostConfig C64HostConfig { get; set; } + + public EmulatorConfig() + { + Monitor = new(); + C64HostConfig = new(); + } + + public void Validate(SystemList systemList) + { + if (!systemList.Systems.Contains(DefaultEmulator)) + throw new DotNet6502Exception($"Setting {nameof(DefaultEmulator)} value {DefaultEmulator} is not supported. Valid values are: {string.Join(',', systemList.Systems)}"); + //Monitor.Validate(); + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64HostConfig.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64HostConfig.cs new file mode 100644 index 000000000..d1f12f42e --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64HostConfig.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Serialization; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64.Config; + +namespace Highbyte.DotNet6502.Util.MCPServer.Emulator.SystemSetup; + +public class C64HostConfig : IHostSystemConfig, ICloneable +{ + public const string ConfigSectionName = "Highbyte.DotNet6502.C64.MCPServer"; + + private C64SystemConfig _systemConfig; + ISystemConfig IHostSystemConfig.SystemConfig => _systemConfig; + public C64SystemConfig SystemConfig => _systemConfig; + + [JsonIgnore] + public bool AudioSupported => false; + + private bool _isDirty = false; + [JsonIgnore] + public bool IsDirty => _isDirty; + public void ClearDirty() + { + _isDirty = false; + } + + public void Validate() + { + if (!IsValid(out List validationErrors)) + throw new DotNet6502Exception($"Config errors: {string.Join(',', validationErrors)}"); + } + + public bool IsValid(out List validationErrors) + { + validationErrors = new List(); + + SystemConfig.IsValid(out var systemConfigValidationErrors); + validationErrors.AddRange(systemConfigValidationErrors); + + return validationErrors.Count == 0; + } + + public C64HostConfig() + { + _systemConfig = new(); + } + + public new object Clone() + { + var clone = (C64HostConfig)MemberwiseClone(); + clone._systemConfig = (C64SystemConfig)SystemConfig.Clone(); + return clone; + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64Setup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64Setup.cs new file mode 100644 index 000000000..a6e9ed967 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/SystemSetup/C64Setup.cs @@ -0,0 +1,79 @@ +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Highbyte.DotNet6502.Systems.Commodore64.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.Util.MCPServer.Emulator.SystemSetup; +internal class C64Setup : ISystemConfigurer +{ + public string SystemName => C64.SystemName; + + public Task> GetConfigurationVariants(IHostSystemConfig hostSystemConfig) => Task.FromResult(s_systemVariants); + public List ConfigurationVariants => s_systemVariants; + + private static readonly List s_systemVariants = C64ModelInventory.C64Models.Keys.ToList(); + + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _configuration; + + + internal C64Setup(ILoggerFactory loggerFactory, IConfiguration configuration) + { + _loggerFactory = loggerFactory; + _configuration = configuration; + } + + public Task GetNewHostSystemConfig() + { + var c64HostConfig = new C64HostConfig(); + _configuration.GetSection($"{C64HostConfig.ConfigSectionName}").Bind(c64HostConfig); + + // TODO: Why is list of ROMs are duplicated when binding from appsettings.json? + // This is a workaround to remove duplicates. + c64HostConfig.SystemConfig.ROMs = c64HostConfig.SystemConfig.ROMs.DistinctBy(p => p.Name).ToList(); + + return Task.FromResult(c64HostConfig); + } + + public Task PersistHostSystemConfig(IHostSystemConfig hostSystemConfig) + { + // TODO: Should user settings be persisted? If so method GetNewHostSystemConfig() also needs to be updated to read from there instead of appsettings.json. + return Task.CompletedTask; + } + + public Task BuildSystem(string configurationVariant, IHostSystemConfig hostSystemConfig) + { + var c64HostSystemConfig = (C64HostConfig)hostSystemConfig; + + var c64Config = new C64Config + { + C64Model = configurationVariant, + Vic2Model = C64ModelInventory.C64Models[configurationVariant].Vic2Models.First().Name, // NTSC, NTSC_old, PAL + AudioEnabled = c64HostSystemConfig.SystemConfig.AudioEnabled, + KeyboardJoystickEnabled = c64HostSystemConfig.SystemConfig.KeyboardJoystickEnabled, + KeyboardJoystick = c64HostSystemConfig.SystemConfig.KeyboardJoystick, + ROMs = c64HostSystemConfig.SystemConfig.ROMs, + ROMDirectory = c64HostSystemConfig.SystemConfig.ROMDirectory, + }; + + var c64 = C64.BuildC64(c64Config, _loggerFactory); + return Task.FromResult(c64); + } + + public Task BuildSystemRunner( + ISystem system, + IHostSystemConfig hostSystemConfig, + NullRenderContext renderContext, + NullInputHandlerContext inputHandlerContext, + NullAudioHandlerContext audioHandlerContext + ) + { + var c64 = (C64)system; + var renderer = new NullRenderer(c64); + var inputHandler = new NullInputHandler(c64); + var audioHandler = new NullAudioHandler(c64); + return Task.FromResult(new SystemRunner(c64, renderer, inputHandler, audioHandler)); + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj new file mode 100644 index 000000000..8cc63e69a --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj @@ -0,0 +1,31 @@ + + + + Exe + net9.0 + enable + enable + + + + portable + true + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs new file mode 100644 index 000000000..3e28e8bcc --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs @@ -0,0 +1,69 @@ +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Systems.Logging.InMem; +using Highbyte.DotNet6502.Util.MCPServer.Emulator; +using Highbyte.DotNet6502.Util.MCPServer.Emulator.SystemSetup; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddConsole(consoleLogOptions => +{ + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; +}); + +builder.Configuration + .AddJsonFile("appsettings.json") + .AddJsonFile("appsettings.Development.json", optional: true); + +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(); + +// ---------- +// Register the emulator EmbeddedMCPHostApp as singleton service +// ---------- +var configuration = builder.Configuration; + +builder.Services.AddSingleton((sp) => +{ + // ---------- + // Create emulator logging + // ---------- + DotNet6502InMemLogStore logStore = new() { WriteDebugMessage = true }; + var logConfig = new DotNet6502InMemLoggerConfiguration(logStore); + var loggerFactory = LoggerFactory.Create(builder => + { + logConfig.LogLevel = LogLevel.Information; // LogLevel.Debug, LogLevel.Information, + builder.AddInMem(logConfig); + builder.SetMinimumLevel(LogLevel.Trace); + }); + + // ---------- + // Get emulator host config + // ---------- + var emulatorConfig = new EmulatorConfig(); + configuration.GetSection(EmulatorConfig.ConfigSectionName).Bind(emulatorConfig); + + // ---------- + // Get systems + // ---------- + var systemList = new SystemList(); + var c64Setup = new C64Setup(loggerFactory, configuration); + systemList.AddSystem(c64Setup); + + // ---------- + // Create & init emulator host app + // ---------- + var embeddedMCPHostApp = new EmbeddedMCPHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, configuration); + embeddedMCPHostApp.Init(); + embeddedMCPHostApp.SelectSystem(C64.SystemName).Wait(); + + return embeddedMCPHostApp; +}); + +await builder.Build().RunAsync(); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/appsettings.json b/src/utils/Highbyte.DotNet6502.Util.MCPServer/appsettings.json new file mode 100644 index 000000000..35372e0bf --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/appsettings.json @@ -0,0 +1,29 @@ +{ + "Highbyte.DotNet6502.MCPServer": { + "DefaultEmulator": "C64" + }, + + "Highbyte.DotNet6502.C64.MCPServer": { + + "SystemConfig": { + "ROMDirectory": "%HOME%/Downloads/C64", + "ROMs": [ + { + "Name": "basic", + "File": "basic.901226-01.bin" + }, + { + "Name": "kernal", + "File": "kernal.901227-03.bin" + }, + { + "Name": "chargen", + "File": "characters.901225-01.bin" + } + ], + + "ColorMapName": "Default", + "AudioEnabled": false + } + } +} \ No newline at end of file From 844bcfbb76c2fac8bf9ac85411662677cccf16ba Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 22 Jun 2025 17:52:45 +0200 Subject: [PATCH 02/17] Bump Microsoft NuGet packages. Add support for MCP server control of emulator running in SadConsole UI app on background thread. --- .vscode/mcp.json | 25 +- .../Highbyte.DotNet6502.App.SadConsole.csproj | 8 +- .../Program.cs | 53 +++- .../SadConsoleHostApp.cs | 4 +- ...ghbyte.DotNet6502.App.SilkNetNative.csproj | 4 +- .../Highbyte.DotNet6502.App.WASM.csproj | 8 +- .../Highbyte.DotNet6502.AI.csproj | 2 +- .../Highbyte.DotNet6502.Impl.AspNet.csproj | 2 +- .../Highbyte.DotNet6502.Systems/HostApp.cs | 55 +++- .../Highbyte.DotNet6502.Systems/IHostApp.cs | 45 +++ .../Highbyte.DotNet6502.csproj | 8 +- .../C64Tool.cs | 288 ++++++++++++------ .../Emulator/EmbeddedMCPHostApp.cs | 2 + .../Program.cs | 2 +- .../Highbyte.DotNet6502.Systems.Tests.csproj | 4 +- .../Highbyte.DotNet6502.Tests.csproj | 4 +- 16 files changed, 380 insertions(+), 134 deletions(-) create mode 100644 src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json index b5900ca51..8548511a5 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,17 +1,24 @@ { "inputs": [], "servers": { + // "DotNet6502": { + // "type": "stdio", + // "command": "dotnet", + // "args": [ + // "run", + // "--project", + // "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj", + // "--configuration", + // "Debug" + // ], + // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" + // } + "DotNet6502": { "type": "stdio", - "command": "dotnet", - "args": [ - "run", - "--project", - "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj", - "--configuration", - "Debug" - ], - "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" + "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" } + } } \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj index fc43ba5f6..557e2fe08 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj @@ -29,12 +29,14 @@ + - - - + + + + diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index 53d47eb35..0beaed068 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -5,7 +5,9 @@ using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Logging.InMem; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; // ---------- // Get config file @@ -22,7 +24,6 @@ builder.AddUserSecrets(); } - IConfiguration Configuration = builder.Build(); // ---------- @@ -37,6 +38,27 @@ builder.SetMinimumLevel(LogLevel.Trace); }); +// ---------- +// Setup DI +// ---------- +var services = new ServiceCollection(); + +// Register configuration and logging to DI +services.AddSingleton(Configuration); +services.AddLogging(loggingBuilder => +{ + loggingBuilder.AddInMem(logConfig); + loggingBuilder.SetMinimumLevel(LogLevel.Trace); +}); + +// TODO: Register your own services here +// services.AddSingleton(); + +var serviceProvider = services.BuildServiceProvider(); + +// Example: resolve loggerFactory from DI if needed +// var loggerFactory = serviceProvider.GetRequiredService(); + // ---------- // Get emulator host config // ---------- @@ -54,10 +76,35 @@ var genericComputerSetup = new GenericComputerSetup(loggerFactory, Configuration); systemList.AddSystem(genericComputerSetup); + // ---------- -// Start SadConsoleHostApp +// Init SadConsoleHostApp // ---------- emulatorConfig.Validate(systemList); - var silkNetHostApp = new SadConsoleHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, Configuration); + +// ---------- +// Start MCP server as a background host +// ---------- +Task.Run(async () => +{ + var mcpBuilder = Host.CreateApplicationBuilder(); + mcpBuilder.Logging.AddConsole(consoleLogOptions => + { + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; + }); + + mcpBuilder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(typeof(Highbyte.DotNet6502.Util.MCPServer.C64Tool).Assembly); + + mcpBuilder.Services.AddSingleton((sp) => silkNetHostApp); + await mcpBuilder.Build().RunAsync(); +}); + +// ---------- +// Start SadConsoleHostApp +// ---------- silkNetHostApp.Run(); diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index 5d96f078c..50cb74b9a 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -75,7 +75,6 @@ protected virtual void OnMonitorStateChange(bool monitorEnabled) private int _logsFrameCount = 0; private DrawImage _logoDrawImage; - /// /// Constructor /// @@ -345,6 +344,9 @@ public override void OnAfterClose() /// private void UpdateSadConsole(object? sender, GameHost gameHost) { + // Process any UI actions that have been queued up from other threads + ExternalControlProcessUIActions(); + // Handle UI-specific keyboard inputs such as toggle monitor, info, etc. HandleUIKeyboardInput().Wait(); diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj index b2ff293e4..bd48a68b3 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj @@ -27,8 +27,8 @@ - - + + diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj index fd7f29277..99a4af9e2 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj @@ -32,12 +32,12 @@ - - - + + + - + diff --git a/src/libraries/Highbyte.DotNet6502.AI/Highbyte.DotNet6502.AI.csproj b/src/libraries/Highbyte.DotNet6502.AI/Highbyte.DotNet6502.AI.csproj index 8c9954edd..07a5f7e03 100644 --- a/src/libraries/Highbyte.DotNet6502.AI/Highbyte.DotNet6502.AI.csproj +++ b/src/libraries/Highbyte.DotNet6502.AI/Highbyte.DotNet6502.AI.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj index 8961be646..244468053 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index ec6562ea4..374b3a0e4 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -1,6 +1,7 @@ -using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; using Highbyte.DotNet6502.Systems.Instrumentation; using Highbyte.DotNet6502.Systems.Instrumentation.Stats; +using Microsoft.Extensions.Logging; namespace Highbyte.DotNet6502.Systems; @@ -15,7 +16,7 @@ public enum EmulatorState { Uninitialized, Running, Paused } /// /// /// -public class HostApp +public class HostApp : IHostApp where TRenderContext : IRenderContext where TInputHandlerContext : IInputHandlerContext where TAudioHandlerContext : IAudioHandlerContext @@ -52,13 +53,13 @@ public IHostSystemConfig CurrentHostSystemConfig throw new DotNet6502Exception("Internal error. No system selected yet. Call SelectSystem() first."); return _currentHostSystemConfig; } - private set + set { _currentHostSystemConfig = value; } } - protected List GetHostSystemConfigs() + public List GetHostSystemConfigs() { var list = new List(); foreach (var system in AvailableSystemNames) @@ -374,4 +375,50 @@ private void InitInstrumentation(ISystem system) .Union(_systemRunner.InputHandler.Instrumentations.Stats.Select(x => (Name: $"{_hostName}-{InputTimeStatName}-{x.Name}", x.Stat))) .ToList(); } + + + private readonly ConcurrentQueue _uiActions = new(); + + + public virtual bool ExternalControlDirectInvoke { get; } = false; + public Task ExternalControlInvokeOnUIThread(Action action) + { + // If HostApp is running on same thread as the external control code, execute the action directly. + if (ExternalControlDirectInvoke) + { + try + { + action(); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + // Otherwise, enqueue the action to be executed on the UI thread (see ExternalControlProcessUIActions); + var tcs = new TaskCompletionSource(); + _uiActions.Enqueue(() => + { + try + { + action(); + tcs.SetResult(null); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + return tcs.Task; + } + + public void ExternalControlProcessUIActions() + { + while (_uiActions.TryDequeue(out var action)) + { + action(); + } + } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs new file mode 100644 index 000000000..422c77c67 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs @@ -0,0 +1,45 @@ +using Highbyte.DotNet6502.Systems.Instrumentation.Stats; + +namespace Highbyte.DotNet6502.Systems; + +public interface IHostApp +{ + public string SelectedSystemName { get; } + + public SystemRunner? CurrentSystemRunner { get; } + public ISystem? CurrentRunningSystem { get; } + + public EmulatorState EmulatorState { get; } + + public IHostSystemConfig CurrentHostSystemConfig { get; } + public List GetHostSystemConfigs(); + + public Task SelectSystem(string systemName); + public Task SelectSystemConfigurationVariant(string configurationVariant); + + public Task Start(); + public void Pause(); + public void Stop(); + public Task Reset(); + + public void Close(); + public void RunEmulatorOneFrame(); + public void DrawFrame(); + + public Task IsSystemConfigValid(); + public Task<(bool, List validationErrors)> IsValidConfigWithDetails(); + public Task IsAudioSupported(); + public Task IsAudioEnabled(); + public Task SetAudioEnabled(bool enabled); + + public Task GetSelectedSystem(); + + public void UpdateHostSystemConfig(IHostSystemConfig newConfig); + public Task PersistCurrentHostSystemConfig(); + + public List<(string name, IStat stat)> GetStats(); + + public bool ExternalControlDirectInvoke { get; } + public Task ExternalControlInvokeOnUIThread(Action action); + public void ExternalControlProcessUIActions(); +} diff --git a/src/libraries/Highbyte.DotNet6502/Highbyte.DotNet6502.csproj b/src/libraries/Highbyte.DotNet6502/Highbyte.DotNet6502.csproj index 9703f7dea..26f1a251a 100644 --- a/src/libraries/Highbyte.DotNet6502/Highbyte.DotNet6502.csproj +++ b/src/libraries/Highbyte.DotNet6502/Highbyte.DotNet6502.csproj @@ -24,10 +24,10 @@ - - - - + + + + diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs index d1a4f239e..7892df615 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs @@ -1,187 +1,281 @@ using System.ComponentModel; -using Highbyte.DotNet6502; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Util.MCPServer.Contract; -using Highbyte.DotNet6502.Util.MCPServer.Emulator; using Highbyte.DotNet6502.Utils; using ModelContextProtocol.Server; +namespace Highbyte.DotNet6502.Util.MCPServer; + [McpServerToolType] public static class C64Tool { [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] - public static EmulatorState GetState(EmbeddedMCPHostApp hostApp) + public static async Task GetState(IHostApp hostApp) { - AssertEmulatorIsC64(hostApp); - return hostApp.EmulatorState; + EmulatorState emulatorState = default; + + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertEmulatorIsC64(hostApp); + emulatorState = hostApp.EmulatorState; + }); + + return emulatorState; } [McpServerTool, Description("Starts C64 emulator.")] - public static async Task Start(EmbeddedMCPHostApp hostApp) + public static async Task Start(IHostApp hostApp) { - AssertC64EmulatorIsPausedOrUninitialzied(hostApp); - await hostApp.Start(); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + await hostApp.Start(); + }); } [McpServerTool, Description("Pause C64 emulator")] - public static async Task Pause(EmbeddedMCPHostApp hostApp) + public static async Task Pause(IHostApp hostApp) { - AssertC64EmulatorIsRunning(hostApp); - hostApp.Pause(); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + hostApp.Pause(); + }); } [McpServerTool, Description("Stop C64 emulator")] - public static async Task Stop(EmbeddedMCPHostApp hostApp) + public static async Task Stop(IHostApp hostApp) { - AssertC64EmulatorIsRunningOrPaused(hostApp); - hostApp.Stop(); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunningOrPaused(hostApp); + hostApp.Stop(); + }); } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfSeconds(EmbeddedMCPHostApp hostApp, int numberOfSeconds) + public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) { - AssertC64EmulatorIsRunning(hostApp); - if (numberOfSeconds <= 0) throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); - var c64 = GetC64(hostApp); - //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? - int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); - await RunNumberOfFrames(hostApp, numberOfFrames); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? + int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); + await RunNumberOfFrames(hostApp, numberOfFrames); + }); } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfFrames(EmbeddedMCPHostApp hostApp, int numberOfFrames) + public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) { - AssertC64EmulatorIsRunning(hostApp); - if (numberOfFrames <= 0) throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); - for (int i = 0; i < numberOfFrames; i++) - hostApp.RunEmulatorOneFrame(); + + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + + for (int i = 0; i < numberOfFrames; i++) + hostApp.RunEmulatorOneFrame(); + }); } [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] - public static async Task RunNumberOfInstructions(EmbeddedMCPHostApp hostApp, int numberOfInstructions) + public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) { - AssertC64EmulatorIsRunning(hostApp); - if (numberOfInstructions <= 0) throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); - for (int i = 0; i < numberOfInstructions; i++) - hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + + for (int i = 0; i < numberOfInstructions; i++) + hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + }); } [McpServerTool, Description("Returns value of specified memory address in C64 emulator")] - public static byte ReadMemory(EmbeddedMCPHostApp hostApp, ushort address) + public static async Task ReadMemory(IHostApp hostApp, ushort address) { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - var value = c64.Mem[address]; + byte value = 0; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + value = c64.Mem[address]; + }); return value; } - [McpServerTool, Description("Returns array of values of from memory start address up to specified length in C64 emulator")] - public static byte[] ReadMemoryRange(EmbeddedMCPHostApp hostApp, ushort startAddress, ushort length) + [McpServerTool, Description("Returns a range of values from memory start address up to specified length in C64 emulator")] + public static async Task ReadMemoryRange(IHostApp hostApp, ushort startAddress, ushort length) { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (length <= 0) throw new ArgumentException("Length must be greater than zero.", nameof(length)); - if (startAddress + length > c64.Mem.Size) - length = (ushort)(c64.Mem.Size - startAddress); - var values = c64.Mem.ReadData(startAddress, length); + byte[] values = null!; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + if (startAddress + length > c64.Mem.Size) + length = (ushort)(c64.Mem.Size - startAddress); + values = c64.Mem.ReadData(startAddress, length); + }); return values; + + } + + [McpServerTool, Description("Writes value at specified memory address in C64 emulator")] + public static async Task WriteMemory(IHostApp hostApp, ushort address, byte value) + { + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + c64.Mem[address] = value; + }); } - [McpServerTool, Description("Sets value at specified memory address in C64 emulator")] - public static void WriteMemory(EmbeddedMCPHostApp hostApp, ushort address, byte value) + /// + /// + /// + /// + /// Array of bytes to write to memory> + [McpServerTool, Description("Writes a range of values (byte array) starting at specified memory address in C64 emulator. Expects 'values' as an array of integers.")] + public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - c64.Mem[address] = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + if (address + values.Length > c64.Mem.Size) + values = values.Take(c64.Mem.Size - address).ToArray(); + c64.Mem.StoreData(address, values); + }); } - [McpServerTool, Description("Sets array of values starting at specified memory address in C64 emulator. Use this if multiple values need to be set in sequence.")] - public static void WriteMemory(EmbeddedMCPHostApp hostApp, ushort address, byte[] values) + /// + /// + /// + /// + /// Sequence of 8-bit bytes in hex separated by space to write to memory> + [McpServerTool, Description("Writes a range of values starting at specified memory address in C64 emulator.")] + public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (address + values.Length > c64.Mem.Size) - values = values.Take(c64.Mem.Size - address).ToArray(); - c64.Mem.StoreData(address, values); + // Convert hex string to byte array + if (string.IsNullOrWhiteSpace(hexValues)) + throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); + var values = hexValues + .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) + .Select(hex => Convert.ToByte(hex, 16)) + .ToArray(); + + await WriteMemoryRange(hostApp, address, values); } + [McpServerTool, Description("Returns the current value of the C64 CPU registers: A, X, Y, PS, PC, SP")] - public static CPURegisters GetCPURegisters(EmbeddedMCPHostApp hostApp) + public static async Task GetCPURegisters(IHostApp hostApp) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - - return new CPURegisters + CPURegisters cpuRegisters = null!; + await hostApp.ExternalControlInvokeOnUIThread(() => { - A = cpu.A, - X = cpu.X, - Y = cpu.Y, - PC = cpu.PC, - SP = cpu.SP, - ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) - }; + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpuRegisters = new CPURegisters + { + A = cpu.A, + X = cpu.X, + Y = cpu.Y, + PC = cpu.PC, + SP = cpu.SP, + ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) + }; + }); + return cpuRegisters; } [McpServerTool, Description("Sets the CPU register A")] - public static void SetCPURegisterA(EmbeddedMCPHostApp hostApp, byte value) + public static async Task SetCPURegisterA(IHostApp hostApp, byte value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.A = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.A = value; + }); } [McpServerTool, Description("Sets the CPU register X")] - public static void SetCPURegisterX(EmbeddedMCPHostApp hostApp, byte value) + public static async Task SetCPURegisterX(IHostApp hostApp, byte value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.X = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.X = value; + }); } [McpServerTool, Description("Sets the CPU register Y")] - public static void SetCPURegisterY(EmbeddedMCPHostApp hostApp, byte value) + public static async Task SetCPURegisterY(IHostApp hostApp, byte value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.Y = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.Y = value; + }); } [McpServerTool, Description("Sets the CPU register PC (Program Counter)")] - public static void SetCPURegisterPC(EmbeddedMCPHostApp hostApp, ushort value) + public static async Task SetCPURegisterPC(IHostApp hostApp, ushort value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.PC = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.PC = value; + }); } [McpServerTool, Description("Sets the CPU register SP (Stack Pointer)")] - public static void SetCPURegisterSP(EmbeddedMCPHostApp hostApp, byte value) + public static async Task SetCPURegisterSP(IHostApp hostApp, byte value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.SP = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.SP = value; + }); } [McpServerTool, Description("Sets the CPU register PS (Processor Status)")] - public static void SetCPURegisterPS(EmbeddedMCPHostApp hostApp, byte value) + public static async Task SetCPURegisterPS(IHostApp hostApp, byte value) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.ProcessorStatus.Value = value; + await hostApp.ExternalControlInvokeOnUIThread(() => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.ProcessorStatus.Value = value; + }); } - private static void AssertC64EmulatorIsRunning(EmbeddedMCPHostApp hostApp) + private static void AssertC64EmulatorIsRunning(IHostApp hostApp) { AssertEmulatorIsC64(hostApp); if (hostApp.EmulatorState != EmulatorState.Running) @@ -190,7 +284,7 @@ private static void AssertC64EmulatorIsRunning(EmbeddedMCPHostApp hostApp) } } - private static void AssertC64EmulatorIsRunningOrPaused(EmbeddedMCPHostApp hostApp) + private static void AssertC64EmulatorIsRunningOrPaused(IHostApp hostApp) { AssertEmulatorIsC64(hostApp); if (hostApp.EmulatorState != EmulatorState.Running && hostApp.EmulatorState != EmulatorState.Paused) @@ -199,7 +293,7 @@ private static void AssertC64EmulatorIsRunningOrPaused(EmbeddedMCPHostApp hostAp } } - private static void AssertC64EmulatorIsPausedOrUninitialzied(EmbeddedMCPHostApp hostApp) + private static void AssertC64EmulatorIsPausedOrUninitialzied(IHostApp hostApp) { AssertEmulatorIsC64(hostApp); if (hostApp.EmulatorState != EmulatorState.Paused && hostApp.EmulatorState != EmulatorState.Uninitialized) @@ -208,7 +302,7 @@ private static void AssertC64EmulatorIsPausedOrUninitialzied(EmbeddedMCPHostApp } } - private static void AssertC64EmulatorIsUninitialzied(EmbeddedMCPHostApp hostApp) + private static void AssertC64EmulatorIsUninitialzied(IHostApp hostApp) { AssertEmulatorIsC64(hostApp); if (hostApp.EmulatorState != EmulatorState.Uninitialized) @@ -217,7 +311,7 @@ private static void AssertC64EmulatorIsUninitialzied(EmbeddedMCPHostApp hostApp) } } - private static void AssertEmulatorIsC64(EmbeddedMCPHostApp hostApp) + private static void AssertEmulatorIsC64(IHostApp hostApp) { if (hostApp.SelectedSystemName != C64.SystemName) { @@ -225,7 +319,7 @@ private static void AssertEmulatorIsC64(EmbeddedMCPHostApp hostApp) } } - private static C64 GetC64(EmbeddedMCPHostApp hostApp) + private static C64 GetC64(IHostApp hostApp) { if (hostApp.CurrentRunningSystem is C64 c64) { diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs index ee9244145..4d46433e1 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Emulator/EmbeddedMCPHostApp.cs @@ -122,4 +122,6 @@ public override void OnAfterDrawFrame(bool emulatorRendered) { } } + + public override bool ExternalControlDirectInvoke => true; } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs index 3e28e8bcc..b1c104ce3 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs @@ -29,7 +29,7 @@ // ---------- var configuration = builder.Configuration; -builder.Services.AddSingleton((sp) => +builder.Services.AddSingleton((sp) => { // ---------- // Create emulator logging diff --git a/tests/Highbyte.DotNet6502.Systems.Tests/Highbyte.DotNet6502.Systems.Tests.csproj b/tests/Highbyte.DotNet6502.Systems.Tests/Highbyte.DotNet6502.Systems.Tests.csproj index 58b9474bf..abb0e8b8d 100644 --- a/tests/Highbyte.DotNet6502.Systems.Tests/Highbyte.DotNet6502.Systems.Tests.csproj +++ b/tests/Highbyte.DotNet6502.Systems.Tests/Highbyte.DotNet6502.Systems.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Highbyte.DotNet6502.Tests/Highbyte.DotNet6502.Tests.csproj b/tests/Highbyte.DotNet6502.Tests/Highbyte.DotNet6502.Tests.csproj index 7673259c3..21bbef543 100644 --- a/tests/Highbyte.DotNet6502.Tests/Highbyte.DotNet6502.Tests.csproj +++ b/tests/Highbyte.DotNet6502.Tests/Highbyte.DotNet6502.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive From 6662cec79de3e48713ad9fafffb5518155a1da5f Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 22 Jun 2025 18:14:42 +0200 Subject: [PATCH 03/17] Make sure SadConsole UI is refreshed if any external actions are executed. --- .../SadConsoleHostApp.cs | 21 ++++++++++++++++--- .../Highbyte.DotNet6502.Systems/HostApp.cs | 10 +++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index 50cb74b9a..b2fb6cc1c 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -344,9 +344,6 @@ public override void OnAfterClose() /// private void UpdateSadConsole(object? sender, GameHost gameHost) { - // Process any UI actions that have been queued up from other threads - ExternalControlProcessUIActions(); - // Handle UI-specific keyboard inputs such as toggle monitor, info, etc. HandleUIKeyboardInput().Wait(); @@ -612,4 +609,22 @@ private async Task HandleUIKeyboardInput() } } } + + public override void ExternalControlRefreshUI() + { + // Refresh UI controls + if (_menuConsole != null) + _menuConsole.IsDirty = true; + if (_systemMenuConsole != null) + _systemMenuConsole.IsDirty = true; + if (_sadConsoleEmulatorConsole != null) + _sadConsoleEmulatorConsole.IsDirty = true; + + //if (_monitorConsole != null) + // _monitorConsole.IsDirty = true; + //if (_monitorStatusConsole != null) + // _monitorStatusConsole.IsDirty = true; + //if (_infoConsole != null) + // _infoConsole.IsDirty = true; + } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index 374b3a0e4..b1574cec1 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -263,6 +263,9 @@ public virtual void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool sho } public void RunEmulatorOneFrame() { + // Process any UI actions that have been queued up from other threads + ExternalControlProcessUIActions(); + // Safety check to avoid running emulator if it's not in a running state. if (EmulatorState != EmulatorState.Running) return; @@ -420,5 +423,12 @@ public void ExternalControlProcessUIActions() { action(); } + ExternalControlRefreshUI(); + } + + public virtual void ExternalControlRefreshUI() + { + // This method can be overridden by derived classes to refresh the UI. + // It is called after processing UI actions to ensure the UI is up-to-date. } } From 80eab85b09ff3eb4e3e15a4b5b5ddb8fecb803ad Mon Sep 17 00:00:00 2001 From: Highbyte Date: Sun, 22 Jun 2025 23:04:33 +0200 Subject: [PATCH 04/17] Return error information according to MCP contract when a MCP tools encounters errors. Fix async issue when running commands on HostApp that has been queued from the MCP endpoint. Always return JSON object even for primitives. --- .../Highbyte.DotNet6502.Systems/HostApp.cs | 6 +- .../Highbyte.DotNet6502.Systems/IHostApp.cs | 3 +- .../C64Tool.cs | 580 +++++++++++++----- 3 files changed, 427 insertions(+), 162 deletions(-) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index b1574cec1..8501b223d 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -384,7 +384,7 @@ private void InitInstrumentation(ISystem system) public virtual bool ExternalControlDirectInvoke { get; } = false; - public Task ExternalControlInvokeOnUIThread(Action action) + public Task ExternalControlInvokeOnUIThread(Func action) { // If HostApp is running on same thread as the external control code, execute the action directly. if (ExternalControlDirectInvoke) @@ -402,11 +402,11 @@ public Task ExternalControlInvokeOnUIThread(Action action) // Otherwise, enqueue the action to be executed on the UI thread (see ExternalControlProcessUIActions); var tcs = new TaskCompletionSource(); - _uiActions.Enqueue(() => + _uiActions.Enqueue(async () => { try { - action(); + await action(); tcs.SetResult(null); } catch (Exception ex) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs index 422c77c67..d5340de15 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs @@ -40,6 +40,7 @@ public interface IHostApp public List<(string name, IStat stat)> GetStats(); public bool ExternalControlDirectInvoke { get; } - public Task ExternalControlInvokeOnUIThread(Action action); + public Task ExternalControlInvokeOnUIThread(Func action); + public void ExternalControlProcessUIActions(); } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs index 7892df615..26075ced0 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs @@ -1,8 +1,12 @@ using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Util.MCPServer.Contract; using Highbyte.DotNet6502.Utils; +using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; namespace Highbyte.DotNet6502.Util.MCPServer; @@ -11,143 +15,232 @@ namespace Highbyte.DotNet6502.Util.MCPServer; public static class C64Tool { [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] - public static async Task GetState(IHostApp hostApp) + public static async Task GetState(IHostApp hostApp) { EmulatorState emulatorState = default; - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertEmulatorIsC64(hostApp); - emulatorState = hostApp.EmulatorState; - }); + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertEmulatorIsC64(hostApp); + emulatorState = hostApp.EmulatorState; + }); + + return BuildCallToolDataResult(emulatorState); - return emulatorState; + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Starts C64 emulator.")] - public static async Task Start(IHostApp hostApp) + public static async Task Start(IHostApp hostApp) { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + await hostApp.Start(); + }); + return new CallToolResult(); + } + catch (Exception ex) { - AssertC64EmulatorIsPausedOrUninitialzied(hostApp); - await hostApp.Start(); - }); + return BuildCallToolErrorResult(ex); + } + } [McpServerTool, Description("Pause C64 emulator")] - public static async Task Pause(IHostApp hostApp) + public static async Task Pause(IHostApp hostApp) { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + hostApp.Pause(); + }); + return new CallToolResult(); + } + catch (Exception ex) { - AssertC64EmulatorIsRunning(hostApp); - hostApp.Pause(); - }); + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Stop C64 emulator")] - public static async Task Stop(IHostApp hostApp) + public static async Task Stop(IHostApp hostApp) { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => + try { - AssertC64EmulatorIsRunningOrPaused(hostApp); - hostApp.Stop(); - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunningOrPaused(hostApp); + hostApp.Stop(); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) + public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) { - if (numberOfSeconds <= 0) - throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); - - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? - int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); - await RunNumberOfFrames(hostApp, numberOfFrames); - }); + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfSeconds <= 0) + throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); + + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? + int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); + await RunNumberOfFrames(hostApp, numberOfFrames); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) + public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) { - if (numberOfFrames <= 0) - throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); - - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => + try { - AssertC64EmulatorIsRunning(hostApp); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfFrames <= 0) + throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); + + AssertC64EmulatorIsRunning(hostApp); - for (int i = 0; i < numberOfFrames; i++) - hostApp.RunEmulatorOneFrame(); - }); + for (int i = 0; i < numberOfFrames; i++) + hostApp.RunEmulatorOneFrame(); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] - public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) + public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) { - if (numberOfInstructions <= 0) - throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); - - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => + try { - AssertC64EmulatorIsRunning(hostApp); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfInstructions <= 0) + throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); + + AssertC64EmulatorIsRunning(hostApp); - for (int i = 0; i < numberOfInstructions; i++) - hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); - }); + for (int i = 0; i < numberOfInstructions; i++) + hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Returns value of specified memory address in C64 emulator")] - public static async Task ReadMemory(IHostApp hostApp, ushort address) + public static async Task ReadMemory(IHostApp hostApp, ushort address) { - byte value = 0; - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(() => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - value = c64.Mem[address]; - }); - return value; + try + { + byte value = 0; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + value = c64.Mem[address]; + }); + return BuildCallToolDataResult(value); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } - [McpServerTool, Description("Returns a range of values from memory start address up to specified length in C64 emulator")] - public static async Task ReadMemoryRange(IHostApp hostApp, ushort startAddress, ushort length) + [McpServerTool(UseStructuredContent = true, ReadOnly = true), Description("Returns a range of values from memory start address up to specified length in C64 emulator")] + public static async Task ReadMemoryRange(IHostApp hostApp, ushort startAddress, ushort length) { - if (length <= 0) - throw new ArgumentException("Length must be greater than zero.", nameof(length)); + try + { + byte[] values = null!; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (length <= 0) + throw new ArgumentException("Length must be greater than zero.", nameof(length)); + + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + if (startAddress + length > c64.Mem.Size) + length = (ushort)(c64.Mem.Size - startAddress); + values = c64.Mem.ReadData(startAddress, length); - byte[] values = null!; - await hostApp.ExternalControlInvokeOnUIThread(() => + }); + // Convert a byte array to System.Text.Json.JsonArray + object jsonArray = new JsonArray(values.Select(b => (JsonNode)b).ToArray()); + var result = BuildCallToolDataResult(jsonArray); + + return result; + } + catch (Exception ex) { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (startAddress + length > c64.Mem.Size) - length = (ushort)(c64.Mem.Size - startAddress); - values = c64.Mem.ReadData(startAddress, length); - }); - return values; + return BuildCallToolErrorResult(ex); + } + } - } [McpServerTool, Description("Writes value at specified memory address in C64 emulator")] - public static async Task WriteMemory(IHostApp hostApp, ushort address, byte value) + public static async Task WriteMemory(IHostApp hostApp, ushort address, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - c64.Mem[address] = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + c64.Mem[address] = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } /// @@ -156,16 +249,25 @@ await hostApp.ExternalControlInvokeOnUIThread(() => /// /// Array of bytes to write to memory> [McpServerTool, Description("Writes a range of values (byte array) starting at specified memory address in C64 emulator. Expects 'values' as an array of integers.")] - public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) + public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) { - await hostApp.ExternalControlInvokeOnUIThread(() => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (address + values.Length > c64.Mem.Size) - values = values.Take(c64.Mem.Size - address).ToArray(); - c64.Mem.StoreData(address, values); - }); + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var c64 = GetC64(hostApp); + if (address + values.Length > c64.Mem.Size) + values = values.Take(c64.Mem.Size - address).ToArray(); + c64.Mem.StoreData(address, values); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } /// @@ -174,107 +276,181 @@ await hostApp.ExternalControlInvokeOnUIThread(() => /// /// Sequence of 8-bit bytes in hex separated by space to write to memory> [McpServerTool, Description("Writes a range of values starting at specified memory address in C64 emulator.")] - public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) + public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) { - // Convert hex string to byte array - if (string.IsNullOrWhiteSpace(hexValues)) - throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); - var values = hexValues - .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) - .Select(hex => Convert.ToByte(hex, 16)) - .ToArray(); - - await WriteMemoryRange(hostApp, address, values); + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + // Convert hex string to byte array + if (string.IsNullOrWhiteSpace(hexValues)) + throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); + var values = hexValues + .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) + .Select(hex => Convert.ToByte(hex, 16)) + .ToArray(); + + await WriteMemoryRange(hostApp, address, values); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } - [McpServerTool, Description("Returns the current value of the C64 CPU registers: A, X, Y, PS, PC, SP")] - public static async Task GetCPURegisters(IHostApp hostApp) + public static async Task GetCPURegisters(IHostApp hostApp) { - CPURegisters cpuRegisters = null!; - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpuRegisters = new CPURegisters + CPURegisters cpuRegisters = null!; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => { - A = cpu.A, - X = cpu.X, - Y = cpu.Y, - PC = cpu.PC, - SP = cpu.SP, - ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) - }; - }); - return cpuRegisters; + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpuRegisters = new CPURegisters + { + A = cpu.A, + X = cpu.X, + Y = cpu.Y, + PC = cpu.PC, + SP = cpu.SP, + ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) + }; + }); + return BuildCallToolDataResult(cpuRegisters); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register A")] - public static async Task SetCPURegisterA(IHostApp hostApp, byte value) + public static async Task SetCPURegisterA(IHostApp hostApp, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.A = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.A = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register X")] - public static async Task SetCPURegisterX(IHostApp hostApp, byte value) + public static async Task SetCPURegisterX(IHostApp hostApp, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.X = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.X = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register Y")] - public static async Task SetCPURegisterY(IHostApp hostApp, byte value) + public static async Task SetCPURegisterY(IHostApp hostApp, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.Y = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.Y = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register PC (Program Counter)")] - public static async Task SetCPURegisterPC(IHostApp hostApp, ushort value) + public static async Task SetCPURegisterPC(IHostApp hostApp, ushort value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.PC = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.PC = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register SP (Stack Pointer)")] - public static async Task SetCPURegisterSP(IHostApp hostApp, byte value) + public static async Task SetCPURegisterSP(IHostApp hostApp, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.SP = value; - }); + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.SP = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Sets the CPU register PS (Processor Status)")] - public static async Task SetCPURegisterPS(IHostApp hostApp, byte value) + public static async Task SetCPURegisterPS(IHostApp hostApp, byte value) { - await hostApp.ExternalControlInvokeOnUIThread(() => + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + AssertC64EmulatorIsRunning(hostApp); + var cpu = GetC64(hostApp).CPU; + cpu.ProcessorStatus.Value = value; + }); + return new CallToolResult(); + } + catch (Exception ex) { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.ProcessorStatus.Value = value; - }); + return BuildCallToolErrorResult(ex); + } } + private static void AssertC64EmulatorIsRunning(IHostApp hostApp) { AssertEmulatorIsC64(hostApp); @@ -327,4 +503,92 @@ private static C64 GetC64(IHostApp hostApp) } throw new InvalidOperationException("Current running system is not a C64 instance."); } + + private static CallToolResult BuildCallToolDataResult(object data) + { + // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. + //JsonNode? jsonNode = JsonSerializer.SerializeToNode(data, s_jsonSerializerOptions); + //return new CallToolResult + //{ + // StructuredContent = jsonNode + //}; + Type type = data.GetType(); + bool needsJsonElementName; + if (type.IsEnum || type == typeof(JsonArray)) + { + needsJsonElementName = true; + } + else if (type.IsArray) + { + var elementType = type.GetElementType(); + needsJsonElementName = elementType.IsPrimitive || elementType == typeof(string) || elementType == typeof(decimal); + } + else + { + needsJsonElementName = type.IsPrimitive || type == typeof(string) || type == typeof(decimal); + } + + object objectToSerialize; + if (needsJsonElementName) + { + objectToSerialize = new + { + Data = data + }; + } + else + { + objectToSerialize = data; + } + string json = JsonSerializer.Serialize(objectToSerialize, s_jsonSerializerOptions); + return new CallToolResult + { + Content = new List + { + new TextContentBlock + { + Text = json + } + } + }; + } + + //private static CallToolResult BuildCallToolDataResult(JsonNode jsonNode) + //{ + // // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. + // //return new CallToolResult + // //{ + // // StructuredContent = jsonNode + // //}; + //} + + private static CallToolResult BuildCallToolErrorResult(Exception ex) + { + return new CallToolResult + { + IsError = true, + Content = new List + { + new TextContentBlock + { + Text = $"C64 emulator error: {ex.Message}" + }, + }, + }; + } + + + private static readonly JsonSerializerOptions s_jsonSerializerOptions = BuildJsonSerializerOptions(); + + private static JsonSerializerOptions BuildJsonSerializerOptions() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + //WriteIndented = true, + //DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } } From 605c12a4b8f97778505697872f3e023110fad6b5 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 23 Jun 2025 11:20:59 +0200 Subject: [PATCH 05/17] Simplify code --- .../Program.cs | 37 +- .../C64MemoryTool.cs | 143 +++++ .../C64RegistersTool.cs | 160 +++++ .../C64StateTool.cs | 166 +++++ .../C64Tool.cs | 594 ------------------ .../C64ToolHelper.cs | 152 +++++ .../ToolSetup.cs | 24 + 7 files changed, 648 insertions(+), 628 deletions(-) create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64RegistersTool.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs delete mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index 0beaed068..fa209ecbe 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Hosting; +using Highbyte.DotNet6502.Util.MCPServer; // ---------- // Get config file @@ -31,34 +32,13 @@ // ---------- DotNet6502InMemLogStore logStore = new() { WriteDebugMessage = true }; var logConfig = new DotNet6502InMemLoggerConfiguration(logStore); +logConfig.LogLevel = LogLevel.Information; // LogLevel.Debug, LogLevel.Information var loggerFactory = LoggerFactory.Create(builder => { - logConfig.LogLevel = LogLevel.Information; // LogLevel.Debug, LogLevel.Information, builder.AddInMem(logConfig); builder.SetMinimumLevel(LogLevel.Trace); }); -// ---------- -// Setup DI -// ---------- -var services = new ServiceCollection(); - -// Register configuration and logging to DI -services.AddSingleton(Configuration); -services.AddLogging(loggingBuilder => -{ - loggingBuilder.AddInMem(logConfig); - loggingBuilder.SetMinimumLevel(LogLevel.Trace); -}); - -// TODO: Register your own services here -// services.AddSingleton(); - -var serviceProvider = services.BuildServiceProvider(); - -// Example: resolve loggerFactory from DI if needed -// var loggerFactory = serviceProvider.GetRequiredService(); - // ---------- // Get emulator host config // ---------- @@ -89,18 +69,7 @@ Task.Run(async () => { var mcpBuilder = Host.CreateApplicationBuilder(); - mcpBuilder.Logging.AddConsole(consoleLogOptions => - { - // Configure all logs to go to stderr - consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; - }); - - mcpBuilder.Services - .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(typeof(Highbyte.DotNet6502.Util.MCPServer.C64Tool).Assembly); - - mcpBuilder.Services.AddSingleton((sp) => silkNetHostApp); + mcpBuilder.ConfigureDotNet6502McpServerTools(silkNetHostApp); await mcpBuilder.Build().RunAsync(); }); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs new file mode 100644 index 000000000..d9e47de16 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs @@ -0,0 +1,143 @@ +using System.ComponentModel; +using System.Text.Json.Nodes; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Utils; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.Util.MCPServer; + +[McpServerToolType] +public static class C64MemoryTool +{ + [McpServerTool, Description("Returns value of specified memory address in C64 emulator")] + public static async Task ReadMemory(IHostApp hostApp, ushort address) + { + try + { + byte value = 0; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + value = c64.Mem[address]; + }); + return C64ToolHelper.BuildCallToolDataResult(value); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool(UseStructuredContent = true, ReadOnly = true), Description("Returns a range of values from memory start address up to specified length in C64 emulator")] + public static async Task ReadMemoryRange(IHostApp hostApp, ushort startAddress, ushort length) + { + try + { + byte[] values = null!; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (length <= 0) + throw new ArgumentException("Length must be greater than zero.", nameof(length)); + + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + if (startAddress + length > c64.Mem.Size) + length = (ushort)(c64.Mem.Size - startAddress); + values = c64.Mem.ReadData(startAddress, length); + + }); + // Convert a byte array to System.Text.Json.JsonArray + object jsonArray = new JsonArray(values.Select(b => (JsonNode)b).ToArray()); + var result = C64ToolHelper.BuildCallToolDataResult(jsonArray); + + return result; + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + + [McpServerTool, Description("Writes value at specified memory address in C64 emulator")] + public static async Task WriteMemory(IHostApp hostApp, ushort address, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + c64.Mem[address] = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + /// + /// + /// + /// + /// Array of bytes to write to memory> + [McpServerTool, Description("Writes a range of values (byte array) starting at specified memory address in C64 emulator. Expects 'values' as an array of integers.")] + public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + if (address + values.Length > c64.Mem.Size) + values = values.Take(c64.Mem.Size - address).ToArray(); + c64.Mem.StoreData(address, values); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + /// + /// + /// + /// + /// Sequence of 8-bit bytes in hex separated by space to write to memory> + [McpServerTool, Description("Writes a range of values starting at specified memory address in C64 emulator.")] + public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + // Convert hex string to byte array + if (string.IsNullOrWhiteSpace(hexValues)) + throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); + var values = hexValues + .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) + .Select(hex => Convert.ToByte(hex, 16)) + .ToArray(); + + await WriteMemoryRange(hostApp, address, values); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64RegistersTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64RegistersTool.cs new file mode 100644 index 000000000..5ff604153 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64RegistersTool.cs @@ -0,0 +1,160 @@ +using System.ComponentModel; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Util.MCPServer.Contract; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.Util.MCPServer; + +[McpServerToolType] +public static class C64RegistersTool +{ + [McpServerTool, Description("Returns the current value of the C64 CPU registers: A, X, Y, PS, PC, SP")] + public static async Task GetCPURegisters(IHostApp hostApp) + { + try + { + CPURegisters cpuRegisters = null!; + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpuRegisters = new CPURegisters + { + A = cpu.A, + X = cpu.X, + Y = cpu.Y, + PC = cpu.PC, + SP = cpu.SP, + ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) + }; + }); + return C64ToolHelper.BuildCallToolDataResult(cpuRegisters); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register A")] + public static async Task SetCPURegisterA(IHostApp hostApp, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.A = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register X")] + public static async Task SetCPURegisterX(IHostApp hostApp, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.X = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register Y")] + public static async Task SetCPURegisterY(IHostApp hostApp, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.Y = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register PC (Program Counter)")] + public static async Task SetCPURegisterPC(IHostApp hostApp, ushort value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.PC = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register SP (Stack Pointer)")] + public static async Task SetCPURegisterSP(IHostApp hostApp, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.SP = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Sets the CPU register PS (Processor Status)")] + public static async Task SetCPURegisterPS(IHostApp hostApp, byte value) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var cpu = C64ToolHelper.GetC64(hostApp).CPU; + cpu.ProcessorStatus.Value = value; + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs new file mode 100644 index 000000000..c8411eb9c --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs @@ -0,0 +1,166 @@ +using System.ComponentModel; +using Highbyte.DotNet6502.Systems; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.Util.MCPServer; + +[McpServerToolType] +public static class C64StateTool +{ + [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] + public static async Task GetState(IHostApp hostApp) + { + EmulatorState emulatorState = default; + + try + { + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertEmulatorIsC64(hostApp); + emulatorState = hostApp.EmulatorState; + }); + + return C64ToolHelper.BuildCallToolDataResult(emulatorState); + + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Starts C64 emulator.")] + public static async Task Start(IHostApp hostApp) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + await hostApp.Start(); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + + } + + [McpServerTool, Description("Pause C64 emulator")] + public static async Task Pause(IHostApp hostApp) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + hostApp.Pause(); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Stop C64 emulator")] + public static async Task Stop(IHostApp hostApp) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunningOrPaused(hostApp); + hostApp.Stop(); + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] + public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfSeconds <= 0) + throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); + + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? + int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); + await RunNumberOfFrames(hostApp, numberOfFrames); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] + public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfFrames <= 0) + throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); + + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + + for (int i = 0; i < numberOfFrames; i++) + hostApp.RunEmulatorOneFrame(); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] + public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) + { + try + { + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + if (numberOfInstructions <= 0) + throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); + + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + + for (int i = 0; i < numberOfInstructions; i++) + hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + + }); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs deleted file mode 100644 index 26075ced0..000000000 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64Tool.cs +++ /dev/null @@ -1,594 +0,0 @@ -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using Highbyte.DotNet6502.Systems; -using Highbyte.DotNet6502.Systems.Commodore64; -using Highbyte.DotNet6502.Util.MCPServer.Contract; -using Highbyte.DotNet6502.Utils; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; - -namespace Highbyte.DotNet6502.Util.MCPServer; - -[McpServerToolType] -public static class C64Tool -{ - [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] - public static async Task GetState(IHostApp hostApp) - { - EmulatorState emulatorState = default; - - try - { - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertEmulatorIsC64(hostApp); - emulatorState = hostApp.EmulatorState; - }); - - return BuildCallToolDataResult(emulatorState); - - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Starts C64 emulator.")] - public static async Task Start(IHostApp hostApp) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsPausedOrUninitialzied(hostApp); - await hostApp.Start(); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - - } - - [McpServerTool, Description("Pause C64 emulator")] - public static async Task Pause(IHostApp hostApp) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - hostApp.Pause(); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Stop C64 emulator")] - public static async Task Stop(IHostApp hostApp) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunningOrPaused(hostApp); - hostApp.Stop(); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - if (numberOfSeconds <= 0) - throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); - - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? - int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); - await RunNumberOfFrames(hostApp, numberOfFrames); - - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - if (numberOfFrames <= 0) - throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); - - AssertC64EmulatorIsRunning(hostApp); - - for (int i = 0; i < numberOfFrames; i++) - hostApp.RunEmulatorOneFrame(); - - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] - public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - if (numberOfInstructions <= 0) - throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); - - AssertC64EmulatorIsRunning(hostApp); - - for (int i = 0; i < numberOfInstructions; i++) - hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); - - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Returns value of specified memory address in C64 emulator")] - public static async Task ReadMemory(IHostApp hostApp, ushort address) - { - try - { - byte value = 0; - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - value = c64.Mem[address]; - }); - return BuildCallToolDataResult(value); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool(UseStructuredContent = true, ReadOnly = true), Description("Returns a range of values from memory start address up to specified length in C64 emulator")] - public static async Task ReadMemoryRange(IHostApp hostApp, ushort startAddress, ushort length) - { - try - { - byte[] values = null!; - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - if (length <= 0) - throw new ArgumentException("Length must be greater than zero.", nameof(length)); - - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (startAddress + length > c64.Mem.Size) - length = (ushort)(c64.Mem.Size - startAddress); - values = c64.Mem.ReadData(startAddress, length); - - }); - // Convert a byte array to System.Text.Json.JsonArray - object jsonArray = new JsonArray(values.Select(b => (JsonNode)b).ToArray()); - var result = BuildCallToolDataResult(jsonArray); - - return result; - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - - [McpServerTool, Description("Writes value at specified memory address in C64 emulator")] - public static async Task WriteMemory(IHostApp hostApp, ushort address, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - c64.Mem[address] = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - /// - /// - /// - /// - /// Array of bytes to write to memory> - [McpServerTool, Description("Writes a range of values (byte array) starting at specified memory address in C64 emulator. Expects 'values' as an array of integers.")] - public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var c64 = GetC64(hostApp); - if (address + values.Length > c64.Mem.Size) - values = values.Take(c64.Mem.Size - address).ToArray(); - c64.Mem.StoreData(address, values); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - /// - /// - /// - /// - /// Sequence of 8-bit bytes in hex separated by space to write to memory> - [McpServerTool, Description("Writes a range of values starting at specified memory address in C64 emulator.")] - public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - // Convert hex string to byte array - if (string.IsNullOrWhiteSpace(hexValues)) - throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); - var values = hexValues - .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) - .Select(hex => Convert.ToByte(hex, 16)) - .ToArray(); - - await WriteMemoryRange(hostApp, address, values); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Returns the current value of the C64 CPU registers: A, X, Y, PS, PC, SP")] - public static async Task GetCPURegisters(IHostApp hostApp) - { - try - { - CPURegisters cpuRegisters = null!; - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpuRegisters = new CPURegisters - { - A = cpu.A, - X = cpu.X, - Y = cpu.Y, - PC = cpu.PC, - SP = cpu.SP, - ProcessorStatus = new ProcessorStatus(cpu.ProcessorStatus.Value) - }; - }); - return BuildCallToolDataResult(cpuRegisters); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register A")] - public static async Task SetCPURegisterA(IHostApp hostApp, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.A = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register X")] - public static async Task SetCPURegisterX(IHostApp hostApp, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.X = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register Y")] - public static async Task SetCPURegisterY(IHostApp hostApp, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.Y = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register PC (Program Counter)")] - public static async Task SetCPURegisterPC(IHostApp hostApp, ushort value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.PC = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register SP (Stack Pointer)")] - public static async Task SetCPURegisterSP(IHostApp hostApp, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.SP = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - [McpServerTool, Description("Sets the CPU register PS (Processor Status)")] - public static async Task SetCPURegisterPS(IHostApp hostApp, byte value) - { - try - { - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - AssertC64EmulatorIsRunning(hostApp); - var cpu = GetC64(hostApp).CPU; - cpu.ProcessorStatus.Value = value; - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return BuildCallToolErrorResult(ex); - } - } - - - private static void AssertC64EmulatorIsRunning(IHostApp hostApp) - { - AssertEmulatorIsC64(hostApp); - if (hostApp.EmulatorState != EmulatorState.Running) - { - throw new InvalidOperationException($"C64 emulator is not running. Current state: {hostApp.EmulatorState}"); - } - } - - private static void AssertC64EmulatorIsRunningOrPaused(IHostApp hostApp) - { - AssertEmulatorIsC64(hostApp); - if (hostApp.EmulatorState != EmulatorState.Running && hostApp.EmulatorState != EmulatorState.Paused) - { - throw new InvalidOperationException($"C64 emulator is not running or paused. Current state: {hostApp.EmulatorState}"); - } - } - - private static void AssertC64EmulatorIsPausedOrUninitialzied(IHostApp hostApp) - { - AssertEmulatorIsC64(hostApp); - if (hostApp.EmulatorState != EmulatorState.Paused && hostApp.EmulatorState != EmulatorState.Uninitialized) - { - throw new InvalidOperationException($"C64 emulator not paused uninitialzied. Current state: {hostApp.EmulatorState}"); - } - } - - private static void AssertC64EmulatorIsUninitialzied(IHostApp hostApp) - { - AssertEmulatorIsC64(hostApp); - if (hostApp.EmulatorState != EmulatorState.Uninitialized) - { - throw new InvalidOperationException($"C64 emulator is running or paused. Current state: {hostApp.EmulatorState}"); - } - } - - private static void AssertEmulatorIsC64(IHostApp hostApp) - { - if (hostApp.SelectedSystemName != C64.SystemName) - { - throw new InvalidOperationException("Current emulated system is not a C64 instance."); - } - } - - private static C64 GetC64(IHostApp hostApp) - { - if (hostApp.CurrentRunningSystem is C64 c64) - { - return c64; - } - throw new InvalidOperationException("Current running system is not a C64 instance."); - } - - private static CallToolResult BuildCallToolDataResult(object data) - { - // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. - //JsonNode? jsonNode = JsonSerializer.SerializeToNode(data, s_jsonSerializerOptions); - //return new CallToolResult - //{ - // StructuredContent = jsonNode - //}; - Type type = data.GetType(); - bool needsJsonElementName; - if (type.IsEnum || type == typeof(JsonArray)) - { - needsJsonElementName = true; - } - else if (type.IsArray) - { - var elementType = type.GetElementType(); - needsJsonElementName = elementType.IsPrimitive || elementType == typeof(string) || elementType == typeof(decimal); - } - else - { - needsJsonElementName = type.IsPrimitive || type == typeof(string) || type == typeof(decimal); - } - - object objectToSerialize; - if (needsJsonElementName) - { - objectToSerialize = new - { - Data = data - }; - } - else - { - objectToSerialize = data; - } - string json = JsonSerializer.Serialize(objectToSerialize, s_jsonSerializerOptions); - return new CallToolResult - { - Content = new List - { - new TextContentBlock - { - Text = json - } - } - }; - } - - //private static CallToolResult BuildCallToolDataResult(JsonNode jsonNode) - //{ - // // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. - // //return new CallToolResult - // //{ - // // StructuredContent = jsonNode - // //}; - //} - - private static CallToolResult BuildCallToolErrorResult(Exception ex) - { - return new CallToolResult - { - IsError = true, - Content = new List - { - new TextContentBlock - { - Text = $"C64 emulator error: {ex.Message}" - }, - }, - }; - } - - - private static readonly JsonSerializerOptions s_jsonSerializerOptions = BuildJsonSerializerOptions(); - - private static JsonSerializerOptions BuildJsonSerializerOptions() - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - //WriteIndented = true, - //DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - options.Converters.Add(new JsonStringEnumConverter()); - return options; - } -} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs new file mode 100644 index 000000000..f1587c70a --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs @@ -0,0 +1,152 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using ModelContextProtocol.Protocol; + +namespace Highbyte.DotNet6502.Util.MCPServer; + +public static class C64ToolHelper +{ + public static void AssertC64EmulatorIsRunning(IHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Running) + { + throw new InvalidOperationException($"C64 emulator is not running. Current state: {hostApp.EmulatorState}"); + } + } + + public static void AssertC64EmulatorIsRunningOrPaused(IHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Running && hostApp.EmulatorState != EmulatorState.Paused) + { + throw new InvalidOperationException($"C64 emulator is not running or paused. Current state: {hostApp.EmulatorState}"); + } + } + + public static void AssertC64EmulatorIsPausedOrUninitialzied(IHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Paused && hostApp.EmulatorState != EmulatorState.Uninitialized) + { + throw new InvalidOperationException($"C64 emulator not paused uninitialzied. Current state: {hostApp.EmulatorState}"); + } + } + + public static void AssertC64EmulatorIsUninitialzied(IHostApp hostApp) + { + AssertEmulatorIsC64(hostApp); + if (hostApp.EmulatorState != EmulatorState.Uninitialized) + { + throw new InvalidOperationException($"C64 emulator is running or paused. Current state: {hostApp.EmulatorState}"); + } + } + + public static void AssertEmulatorIsC64(IHostApp hostApp) + { + if (hostApp.SelectedSystemName != C64.SystemName) + { + throw new InvalidOperationException("Current emulated system is not a C64 instance."); + } + } + + public static C64 GetC64(IHostApp hostApp) + { + if (hostApp.CurrentRunningSystem is C64 c64) + { + return c64; + } + throw new InvalidOperationException("Current running system is not a C64 instance."); + } + + public static CallToolResult BuildCallToolDataResult(object data) + { + // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. + //JsonNode? jsonNode = JsonSerializer.SerializeToNode(data, s_jsonSerializerOptions); + //return new CallToolResult + //{ + // StructuredContent = jsonNode + //}; + Type type = data.GetType(); + bool needsJsonElementName; + if (type.IsEnum || type == typeof(JsonArray)) + { + needsJsonElementName = true; + } + else if (type.IsArray) + { + var elementType = type.GetElementType(); + needsJsonElementName = elementType.IsPrimitive || elementType == typeof(string) || elementType == typeof(decimal); + } + else + { + needsJsonElementName = type.IsPrimitive || type == typeof(string) || type == typeof(decimal); + } + + object objectToSerialize; + if (needsJsonElementName) + { + objectToSerialize = new + { + Data = data + }; + } + else + { + objectToSerialize = data; + } + string json = JsonSerializer.Serialize(objectToSerialize, s_jsonSerializerOptions); + return new CallToolResult + { + Content = new List + { + new TextContentBlock + { + Text = json + } + } + }; + } + + //public static CallToolResult BuildCallToolDataResult(JsonNode jsonNode) + //{ + // // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. + // //return new CallToolResult + // //{ + // // StructuredContent = jsonNode + // //}; + //} + + public static CallToolResult BuildCallToolErrorResult(Exception ex) + { + return new CallToolResult + { + IsError = true, + Content = new List + { + new TextContentBlock + { + Text = $"C64 emulator error: {ex.Message}" + }, + }, + }; + } + + + private static readonly JsonSerializerOptions s_jsonSerializerOptions = BuildJsonSerializerOptions(); + + public static JsonSerializerOptions BuildJsonSerializerOptions() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + //WriteIndented = true, + //DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs new file mode 100644 index 000000000..ae20e6db5 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -0,0 +1,24 @@ +using Highbyte.DotNet6502.Systems; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.Util.MCPServer; +public static class ToolSetup +{ + public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilder builder, IHostApp hostApp) + { + builder.Logging.AddConsole(consoleLogOptions => + { + // Configure all console logs to go to stderr to no interfere with with MCP server STDIO communication + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; + }); + + builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(typeof(C64StateTool).Assembly); + + builder.Services.AddSingleton((sp) => hostApp); + } +} From 632391e9a490eb94653dc8f613dfb32ecd167194 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 23 Jun 2025 11:39:25 +0200 Subject: [PATCH 06/17] Add MCP server support to SilkNetNative host. Make starting MCP server or not configureable. --- src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index 7741930d1..77937704f 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -5,6 +5,7 @@ using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Logging.InMem; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; // Fix for starting in debug mode from VS Code. By default the OS current directory is set to the project folder, not the folder containing the built .exe file... From d93249de73b503a346aa28a878d533c6f1564f09 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 23 Jun 2025 12:00:45 +0200 Subject: [PATCH 07/17] Make enabling MCP configurable --- .vscode/mcp.json | 15 ++++++++----- .../EmulatorConfig.cs | 2 ++ .../Program.cs | 19 ++++++++++------- .../appsettings.json | 4 +++- .../EmulatorConfig.cs | 1 + ...ghbyte.DotNet6502.App.SilkNetNative.csproj | 1 + .../Program.cs | 21 +++++++++++++++++++ .../appsettings.json | 4 +++- 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 8548511a5..6c404e23d 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -14,11 +14,16 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" // } - "DotNet6502": { - "type": "stdio", - "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", - "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" - } + "DotNet6502": { + "type": "stdio", + "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" + } + //"DotNet6502": { + // "type": "stdio", + // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SilkNetNative\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SilkNetNative.exe", + // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SilkNetNative\\bin\\Debug\\net9.0" + //} } } \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs index f7c6c87a3..2c57de4b6 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/EmulatorConfig.cs @@ -49,6 +49,8 @@ public class EmulatorConfig /// public GenericComputerHostConfig GenericComputerHostConfig { get; set; } + public bool MCPServerEnabled { get; set; } + public EmulatorConfig() { UIFont = null; diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index fa209ecbe..a373bed1a 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -61,19 +61,22 @@ // Init SadConsoleHostApp // ---------- emulatorConfig.Validate(systemList); -var silkNetHostApp = new SadConsoleHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, Configuration); +var sadConsoleHostApp = new SadConsoleHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, Configuration); // ---------- -// Start MCP server as a background host +// Start MCP server as a background host if enabled // ---------- -Task.Run(async () => +if (emulatorConfig.MCPServerEnabled) { - var mcpBuilder = Host.CreateApplicationBuilder(); - mcpBuilder.ConfigureDotNet6502McpServerTools(silkNetHostApp); - await mcpBuilder.Build().RunAsync(); -}); + Task.Run(async () => + { + var mcpBuilder = Host.CreateApplicationBuilder(); + mcpBuilder.ConfigureDotNet6502McpServerTools(sadConsoleHostApp); + await mcpBuilder.Build().RunAsync(); + }); +} // ---------- // Start SadConsoleHostApp // ---------- -silkNetHostApp.Run(); +sadConsoleHostApp.Run(); diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json index ef2b47fad..e2a743223 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.json @@ -4,7 +4,9 @@ "UIFont": null, // UI Console font. Leave blank for default SadConsole font. "UIFontSize": "One", // UI consoles font (not Emulator console). Possible values: "Quarter", "Half, "One", "Two", "Three", "Four", "Five" - "DefaultAudioVolumePercent": 20 + "DefaultAudioVolumePercent": 20, + + "MCPServerEnabled": true }, "Highbyte.DotNet6502.C64.SadConsole": { diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/EmulatorConfig.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/EmulatorConfig.cs index a28f25362..c2692111e 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/EmulatorConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/EmulatorConfig.cs @@ -13,6 +13,7 @@ public class EmulatorConfig public float DefaultDrawScale { get; set; } public float CurrentDrawScale { get; set; } public MonitorConfig Monitor { get; set; } + public bool MCPServerEnabled { get; set; } public EmulatorConfig() { diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj index bd48a68b3..411ae28e2 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj @@ -24,6 +24,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index 77937704f..5278460b2 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -4,6 +4,7 @@ using Highbyte.DotNet6502.Impl.SilkNet; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Logging.InMem; +using Highbyte.DotNet6502.Util.MCPServer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -73,6 +74,26 @@ var window = Window.Create(windowOptions); +// ---------- +// Init SilkNetHostApp +// ---------- var silkNetHostApp = new SilkNetHostApp(systemList, loggerFactory, emulatorConfig, window, logStore, logConfig); silkNetHostApp.SelectSystem(emulatorConfig.DefaultEmulator).Wait(); + +// ---------- +// Start MCP server as a background host if enabled +// ---------- +if (emulatorConfig.MCPServerEnabled) +{ + Task.Run(async () => + { + var mcpBuilder = Host.CreateApplicationBuilder(); + mcpBuilder.ConfigureDotNet6502McpServerTools(silkNetHostApp); + await mcpBuilder.Build().RunAsync(); + }); +} + +// ---------- +// Start SilkNetHostApp +// ---------- silkNetHostApp.Run(); diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/appsettings.json b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/appsettings.json index 3205d8e89..b9fd45127 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/appsettings.json +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/appsettings.json @@ -6,7 +6,9 @@ "Monitor": { "MaxLineLength": 100 //"DefaultDirectory": "../../../../../../samples/Assembler/C64/Build" - } + }, + + "MCPServerEnabled": true }, "Highbyte.DotNet6502.C64.SilkNetNative": { From 527d814025734533795f8b4de8f6232b3793abd6 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 23 Jun 2025 14:40:09 +0200 Subject: [PATCH 08/17] Add MCP tool for C64 logs --- .vscode/mcp.json | 26 +++++++++++---- .../MCP/C64SadConsoleTools.cs | 33 +++++++++++++++++++ .../Program.cs | 3 +- .../SadConsoleHostApp.cs | 1 + .../MCP/C64SadConsoleTools.cs | 33 +++++++++++++++++++ .../Program.cs | 3 +- .../SilkNetHostApp.cs | 1 + .../ToolSetup.cs | 20 ++++++++--- 8 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 src/apps/Highbyte.DotNet6502.App.SadConsole/MCP/C64SadConsoleTools.cs create mode 100644 src/apps/Highbyte.DotNet6502.App.SilkNetNative/MCP/C64SadConsoleTools.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 6c404e23d..7d9108aa1 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,7 +1,7 @@ { "inputs": [], "servers": { - // "DotNet6502": { + // "DotNet6502-Embedded-Windows": { // "type": "stdio", // "command": "dotnet", // "args": [ @@ -14,16 +14,28 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" // } - "DotNet6502": { - "type": "stdio", - "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", - "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" - } + // "DotNet6502-SadConsole-Windows": { + // "type": "stdio", + // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" + // } - //"DotNet6502": { + // "DotNet6502-SadConsole-Mac": { + // "type": "stdio", + // "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0/Highbyte.DotNet6502.App.SadConsole", + // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0" + // } + + //"DotNet6502-SilkNet-Windows": { // "type": "stdio", // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SilkNetNative\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SilkNetNative.exe", // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SilkNetNative\\bin\\Debug\\net9.0" //} + + "DotNet6502-SilkNet-Mac": { + "type": "stdio", + "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0/Highbyte.DotNet6502.App.SilkNetNative", + "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0" + } } } \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/MCP/C64SadConsoleTools.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/MCP/C64SadConsoleTools.cs new file mode 100644 index 000000000..3abe840bf --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/MCP/C64SadConsoleTools.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Util.MCPServer; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.App.SadConsole.MCP; + +[McpServerToolType] +public static class C64SadConsoleTools +{ + [McpServerTool, Description("Get C64 emulator log messages")] + public static async Task GetLogMessages(IHostApp hostApp, int numberOfLogMessages) + { + try + { + List logs = null!; + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunningOrPaused(hostApp); + var logStore = ((SadConsoleHostApp)hostApp).LogStore; + logs = logStore.GetLogMessages().Take(numberOfLogMessages).ToList(); + }); + + return C64ToolHelper.BuildCallToolDataResult(logs); + + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs index a373bed1a..6590487bb 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Program.cs @@ -71,7 +71,8 @@ Task.Run(async () => { var mcpBuilder = Host.CreateApplicationBuilder(); - mcpBuilder.ConfigureDotNet6502McpServerTools(sadConsoleHostApp); + mcpBuilder.ConfigureDotNet6502McpServerTools(sadConsoleHostApp, + additionalToolsAssembly: typeof(Highbyte.DotNet6502.App.SadConsole.MCP.C64SadConsoleTools).Assembly); await mcpBuilder.Build().RunAsync(); }); } diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index b2fb6cc1c..2124feee3 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -28,6 +28,7 @@ public class SadConsoleHostApp : HostApp _emulatorConfig; private readonly DotNet6502InMemLogStore _logStore; + public DotNet6502InMemLogStore LogStore => _logStore; private readonly DotNet6502InMemLoggerConfiguration _logConfig; private readonly IConfiguration _configuration; private readonly ILoggerFactory _loggerFactory; diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/MCP/C64SadConsoleTools.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/MCP/C64SadConsoleTools.cs new file mode 100644 index 000000000..c37ec797d --- /dev/null +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/MCP/C64SadConsoleTools.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Util.MCPServer; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.App.SilkNetNative.MCP; + +[McpServerToolType] +public static class C64SilkNetNativeTools +{ + [McpServerTool, Description("Get C64 emulator log messages")] + public static async Task GetLogMessages(IHostApp hostApp, int numberOfLogMessages) + { + try + { + List logs = null!; + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunningOrPaused(hostApp); + var logStore = ((SilkNetHostApp)hostApp).LogStore; + logs = logStore.GetLogMessages().Take(numberOfLogMessages).ToList(); + }); + + return C64ToolHelper.BuildCallToolDataResult(logs); + + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index 5278460b2..dfe1824c3 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -88,7 +88,8 @@ Task.Run(async () => { var mcpBuilder = Host.CreateApplicationBuilder(); - mcpBuilder.ConfigureDotNet6502McpServerTools(silkNetHostApp); + mcpBuilder.ConfigureDotNet6502McpServerTools(silkNetHostApp, + typeof(Highbyte.DotNet6502.App.SilkNetNative.MCP.C64SilkNetNativeTools).Assembly); await mcpBuilder.Build().RunAsync(); }); } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs index 057909f9b..73d1b926a 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetHostApp.cs @@ -22,6 +22,7 @@ public class SilkNetHostApp : HostApp _emulatorConfig; private readonly DotNet6502InMemLogStore _logStore; + public DotNet6502InMemLogStore LogStore => _logStore; private readonly DotNet6502InMemLoggerConfiguration _logConfig; private readonly bool _defaultAudioEnabled; private float _defaultAudioVolumePercent; diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index ae20e6db5..c544421f7 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -1,23 +1,35 @@ +using System.Reflection; using Highbyte.DotNet6502.Systems; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Highbyte.DotNet6502.Util.MCPServer; + public static class ToolSetup { - public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilder builder, IHostApp hostApp) + public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilder builder, IHostApp hostApp, Assembly? additionalToolsAssembly = null) { + // Add MCP server tools from the specified assembly + builder.Services.AddMcpServer() + .WithToolsFromAssembly(additionalToolsAssembly); + + // Add the host app as a singleton service + builder.Services.AddSingleton((sp) => hostApp); builder.Logging.AddConsole(consoleLogOptions => { // Configure all console logs to go to stderr to no interfere with with MCP server STDIO communication consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; }); - builder.Services + var mcpServerBuilder = builder.Services .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(typeof(C64StateTool).Assembly); + .WithStdioServerTransport(); + mcpServerBuilder.WithToolsFromAssembly(typeof(C64StateTool).Assembly); + if (additionalToolsAssembly != null) + { + mcpServerBuilder.WithToolsFromAssembly(additionalToolsAssembly); + } builder.Services.AddSingleton((sp) => hostApp); } From dbb44a3f602249e56da977b7865925f2761e4ad3 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 23 Jun 2025 15:11:03 +0200 Subject: [PATCH 09/17] WIP: Add breakpoint support --- .vscode/mcp.json | 20 +++--- .../C64BreakpointTool.cs | 72 +++++++++++++++++++ .../Services/BreakpointManager.cs | 38 ++++++++++ .../ToolSetup.cs | 1 + 4 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 7d9108aa1..26b68ffb0 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -20,11 +20,11 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" // } - // "DotNet6502-SadConsole-Mac": { - // "type": "stdio", - // "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0/Highbyte.DotNet6502.App.SadConsole", - // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0" - // } + "DotNet6502-SadConsole-Mac": { + "type": "stdio", + "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0/Highbyte.DotNet6502.App.SadConsole", + "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0" + } //"DotNet6502-SilkNet-Windows": { // "type": "stdio", @@ -32,10 +32,10 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SilkNetNative\\bin\\Debug\\net9.0" //} - "DotNet6502-SilkNet-Mac": { - "type": "stdio", - "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0/Highbyte.DotNet6502.App.SilkNetNative", - "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0" - } + // "DotNet6502-SilkNet-Mac": { + // "type": "stdio", + // "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0/Highbyte.DotNet6502.App.SilkNetNative", + // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SilkNetNative/bin/Debug/net9.0" + // } } } \ No newline at end of file diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs new file mode 100644 index 000000000..300e7154c --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs @@ -0,0 +1,72 @@ +using System.ComponentModel; +using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Monitor; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Highbyte.DotNet6502.Util.MCPServer; + +[McpServerToolType] +public static class C64BreakpointTool +{ + [McpServerTool, Description("List all breakpoints in the C64 emulator.")] + public static async Task ListBreakpoints(IHostApp hostApp, BreakpointManager breakpointManager) + { + var breakpoints = breakpointManager.BreakPoints; + var result = new List(); + foreach (var bp in breakpoints) + { + result.Add(new { Address = bp.Key, Enabled = bp.Value.Enabled }); + } + return C64ToolHelper.BuildCallToolDataResult(result); + } + + [McpServerTool, Description("Add a breakpoint at the specified address.")] + public static async Task AddBreakpoint(IHostApp hostApp, BreakpointManager breakpointManager, ushort address) + { + var breakpoints = breakpointManager.BreakPoints; + if (!breakpoints.ContainsKey(address)) + breakpoints.Add(address, new BreakPoint { Enabled = true }); + else + breakpoints[address].Enabled = true; + + EnableOrDisableBreakpointHandling(hostApp, breakpointManager); + return C64ToolHelper.BuildCallToolDataResult(new { Address = address, Enabled = true }); + } + + [McpServerTool, Description("Remove a breakpoint at the specified address.")] + public static async Task RemoveBreakpoint(IHostApp hostApp, BreakpointManager breakpointManager, ushort address) + { + var breakpoints = breakpointManager.BreakPoints; + if (breakpoints.ContainsKey(address)) + breakpoints.Remove(address); + + EnableOrDisableBreakpointHandling(hostApp, breakpointManager); + return C64ToolHelper.BuildCallToolDataResult(new { Address = address, Removed = true }); + } + + [McpServerTool, Description("Remove all breakpoints.")] + public static async Task RemoveAllBreakpoints(IHostApp hostApp, BreakpointManager breakpointManager) + { + var breakpoints = breakpointManager.BreakPoints; + breakpoints.Clear(); + EnableOrDisableBreakpointHandling(hostApp, breakpointManager); + return C64ToolHelper.BuildCallToolDataResult(new { RemovedAll = true }); + } + + private static void EnableOrDisableBreakpointHandling(IHostApp hostApp, BreakpointManager breakpointManager) + { + if (hostApp.CurrentSystemRunner == null) + { + return; // No system runner available, nothing to do. + } + if (breakpointManager.BreakPoints.Count == 0) + { + breakpointManager.DisableBreakpointHandling(hostApp.CurrentSystemRunner); + } + else + { + breakpointManager.EnableBreakpointHandling(hostApp.CurrentSystemRunner); + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs new file mode 100644 index 000000000..fe9609faa --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs @@ -0,0 +1,38 @@ +using Highbyte.DotNet6502.Monitor; +using Highbyte.DotNet6502.Systems; + +public class BreakpointManager +{ + private readonly Dictionary _breakPoints = new(); + + public Dictionary BreakPoints => _breakPoints; + + public void EnableBreakpointHandling(SystemRunner systemRunner) + { + + } + + public void DisableBreakpointHandling(SystemRunner systemRunner) + { + // Implementation to disable breakpoint handling if needed + } + + public void AddBreakpoint(ushort address) + { + if (!_breakPoints.ContainsKey(address)) + _breakPoints.Add(address, new BreakPoint { Enabled = true }); + else + _breakPoints[address].Enabled = true; + } + + public void RemoveBreakpoint(ushort address) + { + if (_breakPoints.ContainsKey(address)) + _breakPoints.Remove(address); + } + + public void RemoveAllBreakpoints() + { + _breakPoints.Clear(); + } +} \ No newline at end of file diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index c544421f7..ec3e57233 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -16,6 +16,7 @@ public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilde // Add the host app as a singleton service builder.Services.AddSingleton((sp) => hostApp); + builder.Services.AddSingleton(); builder.Logging.AddConsole(consoleLogOptions => { // Configure all console logs to go to stderr to no interfere with with MCP server STDIO communication From f099932fc22c10c8b6cbf8715cb6d707dda78879 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Wed, 25 Jun 2025 00:03:20 +0200 Subject: [PATCH 10/17] WIP: MCP server breakpoints and execution control --- .../Highbyte.DotNet6502.Systems/HostApp.cs | 61 ++++++++++++++++--- .../Highbyte.DotNet6502.Systems/IHostApp.cs | 8 ++- .../C64BreakpointTool.cs | 19 +++--- .../Services/BreakpointManager.cs | 52 ++++++++++++++-- 4 files changed, 112 insertions(+), 28 deletions(-) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index 8501b223d..ab19c2f31 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -78,12 +78,17 @@ public List GetHostSystemConfigs() private ElapsedMillisecondsTimedStatSystem? _systemTime; private ElapsedMillisecondsTimedStatSystem? _renderTime; private ElapsedMillisecondsTimedStatSystem? _inputTime; + //private ElapsedMillisecondsTimedStatSystem _audioTime; private readonly Instrumentations _instrumentations = new(); private readonly PerSecondTimedStat _updateFps; private readonly PerSecondTimedStat _renderFps; + private bool _externalControlEnabled = false; + private Func<(bool shouldRun, bool shouldReceiveInput)>? _externalOnBeforeRunEmulatorOneFrame; + private Action? _externalOnAfterRunEmulatorOneFrame; + public HostApp( string hostName, SystemList systemList, @@ -264,13 +269,27 @@ public virtual void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool sho public void RunEmulatorOneFrame() { // Process any UI actions that have been queued up from other threads - ExternalControlProcessUIActions(); + if (_externalControlEnabled) + { + ExternalControlProcessUIActions(); + } // Safety check to avoid running emulator if it's not in a running state. if (EmulatorState != EmulatorState.Running) return; - OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool shouldReceiveInput); + bool shouldRun = false; + bool shouldReceiveInput = false; + if (_externalControlEnabled) + { + if (_externalOnBeforeRunEmulatorOneFrame != null) + (shouldRun, shouldReceiveInput) = _externalOnBeforeRunEmulatorOneFrame(); + } + else + { + OnBeforeRunEmulatorOneFrame(out shouldRun, out shouldReceiveInput); + } + if (!shouldRun) return; @@ -285,7 +304,15 @@ public void RunEmulatorOneFrame() _systemTime!.Start(); var execEvaluatorTriggerResult = _systemRunner!.RunEmulatorOneFrame(); - OnAfterRunEmulatorOneFrame(execEvaluatorTriggerResult); + + if (_externalControlEnabled) + { + _externalOnAfterRunEmulatorOneFrame?.Invoke(execEvaluatorTriggerResult); + } + else + { + OnAfterRunEmulatorOneFrame(execEvaluatorTriggerResult); + } _systemTime!.Stop(); } @@ -380,11 +407,9 @@ private void InitInstrumentation(ISystem system) } - private readonly ConcurrentQueue _uiActions = new(); - - + private readonly ConcurrentQueue _externalControlUIActions = new(); public virtual bool ExternalControlDirectInvoke { get; } = false; - public Task ExternalControlInvokeOnUIThread(Func action) + public Task ExternalControlInvokeOnUIThread(Func action) { // If HostApp is running on same thread as the external control code, execute the action directly. if (ExternalControlDirectInvoke) @@ -402,7 +427,7 @@ public Task ExternalControlInvokeOnUIThread(Func action) // Otherwise, enqueue the action to be executed on the UI thread (see ExternalControlProcessUIActions); var tcs = new TaskCompletionSource(); - _uiActions.Enqueue(async () => + _externalControlUIActions.Enqueue(async () => { try { @@ -419,7 +444,7 @@ public Task ExternalControlInvokeOnUIThread(Func action) public void ExternalControlProcessUIActions() { - while (_uiActions.TryDequeue(out var action)) + while (_externalControlUIActions.TryDequeue(out var action)) { action(); } @@ -431,4 +456,22 @@ public virtual void ExternalControlRefreshUI() // This method can be overridden by derived classes to refresh the UI. // It is called after processing UI actions to ensure the UI is up-to-date. } + + public void EnableExternalControl( + Func<(bool shouldRun, bool shouldReceiveInput)>? externalOnBeforeRunEmulatorOneFrame = null, + Action? externalOnAfterRunEmulatorOneFrame = null) + { + _externalControlEnabled = true; + _externalOnBeforeRunEmulatorOneFrame = externalOnBeforeRunEmulatorOneFrame; + _externalOnAfterRunEmulatorOneFrame = externalOnAfterRunEmulatorOneFrame; + _logger.LogInformation("External control enabled."); + } + + public void DisableExternalControl() + { + _externalControlEnabled = false; + _externalOnBeforeRunEmulatorOneFrame = null; + _externalOnAfterRunEmulatorOneFrame = null; + _logger.LogInformation("External control disabled."); + } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs index d5340de15..f4f332d2a 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs @@ -39,8 +39,14 @@ public interface IHostApp public List<(string name, IStat stat)> GetStats(); + /// + /// Set to true if the host app doesn't have UI thread. Invoking external control actions will then be done on current thread. + /// public bool ExternalControlDirectInvoke { get; } public Task ExternalControlInvokeOnUIThread(Func action); - public void ExternalControlProcessUIActions(); + public void EnableExternalControl( + Func<(bool shouldRun, bool shouldReceiveInput)>? externalOnBeforeRunEmulatorOneFrame = null, + Action? externalOnAfterRunEmulatorOneFrame = null); + public void DisableExternalControl(); } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs index 300e7154c..2db0244dd 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs @@ -30,8 +30,8 @@ public static async Task AddBreakpoint(IHostApp hostApp, Breakpo else breakpoints[address].Enabled = true; - EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return C64ToolHelper.BuildCallToolDataResult(new { Address = address, Enabled = true }); + EnableOrDisableBreakpointHandling(hostApp, breakpointManager); + return new CallToolResult(); } [McpServerTool, Description("Remove a breakpoint at the specified address.")] @@ -42,7 +42,7 @@ public static async Task RemoveBreakpoint(IHostApp hostApp, Brea breakpoints.Remove(address); EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return C64ToolHelper.BuildCallToolDataResult(new { Address = address, Removed = true }); + return new CallToolResult(); } [McpServerTool, Description("Remove all breakpoints.")] @@ -51,22 +51,17 @@ public static async Task RemoveAllBreakpoints(IHostApp hostApp, var breakpoints = breakpointManager.BreakPoints; breakpoints.Clear(); EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return C64ToolHelper.BuildCallToolDataResult(new { RemovedAll = true }); + return new CallToolResult(); } private static void EnableOrDisableBreakpointHandling(IHostApp hostApp, BreakpointManager breakpointManager) { if (hostApp.CurrentSystemRunner == null) - { return; // No system runner available, nothing to do. - } + if (breakpointManager.BreakPoints.Count == 0) - { - breakpointManager.DisableBreakpointHandling(hostApp.CurrentSystemRunner); - } + breakpointManager.DisableBreakpointHandling(hostApp); else - { - breakpointManager.EnableBreakpointHandling(hostApp.CurrentSystemRunner); - } + breakpointManager.EnableBreakpointHandling(hostApp); } } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs index fe9609faa..1188107b0 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs @@ -1,20 +1,31 @@ +using Highbyte.DotNet6502; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; public class BreakpointManager { private readonly Dictionary _breakPoints = new(); - public Dictionary BreakPoints => _breakPoints; + private IExecEvaluator? _originalExecEvaluator; - public void EnableBreakpointHandling(SystemRunner systemRunner) - { + private bool _cpuExecutionPaused = false; + public bool CpuExecutionPaused => _cpuExecutionPaused; + public void EnableBreakpointHandling(IHostApp hostApp) + { + if (hostApp.CurrentSystemRunner == null) + throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); + _originalExecEvaluator = hostApp.CurrentSystemRunner.CustomExecEvaluator; + hostApp.CurrentSystemRunner.SetCustomExecEvaluator(new BreakPointExecEvaluator(_breakPoints)); + hostApp.EnableExternalControl(OnBeforeRunEmulatorOneFrame, OnAfterRunEmulatorOneFrame); } - public void DisableBreakpointHandling(SystemRunner systemRunner) + public void DisableBreakpointHandling(IHostApp hostApp) { - // Implementation to disable breakpoint handling if needed + if (hostApp.CurrentSystemRunner == null) + throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); + hostApp.CurrentSystemRunner.SetCustomExecEvaluator(_originalExecEvaluator); + hostApp.DisableExternalControl(); } public void AddBreakpoint(ushort address) @@ -35,4 +46,33 @@ public void RemoveAllBreakpoints() { _breakPoints.Clear(); } -} \ No newline at end of file + + public void PauseCPUExecution() + { + _cpuExecutionPaused = true; + } + + public void ContinueCPUExecution() + { + _cpuExecutionPaused = false; + } + + private (bool shouldRun, bool shouldReceiveInput) OnBeforeRunEmulatorOneFrame() + { + // If CPU execution is paused, we should not run the emulator. + return (!_cpuExecutionPaused, true); + } + + private void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + if (execEvaluatorTriggerResult.Triggered) + { + // If a breakpoint was hit, pause the CPU execution. + PauseCPUExecution(); + if (execEvaluatorTriggerResult.TriggerType == ExecEvaluatorTriggerReasonType.DebugBreakPoint) + { + //Console.WriteLine($"Breakpoint hit at address: {execEvaluatorTriggerResult.TriggerDescription}"); + } + } + } +} From 2e2932f7442ff99fb1eb2f8f13d732a100cdd349 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Wed, 25 Jun 2025 14:32:01 +0200 Subject: [PATCH 11/17] WIP: Remote control and debug --- .../Highbyte.DotNet6502.Systems/HostApp.cs | 6 +- .../Highbyte.DotNet6502.Systems/IHostApp.cs | 1 + .../C64BreakpointTool.cs | 82 +++++---- .../C64StateTool.cs | 158 +++++++++++++++--- .../C64ToolHelper.cs | 6 + .../Services/BreakpointManager.cs | 52 ------ .../Services/StateManager.cs | 75 +++++++++ .../ToolSetup.cs | 1 + 8 files changed, 266 insertions(+), 115 deletions(-) create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index ab19c2f31..fdc64f359 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -86,6 +86,7 @@ public List GetHostSystemConfigs() private readonly PerSecondTimedStat _renderFps; private bool _externalControlEnabled = false; + public bool ExternalControlEnabled => _externalControlEnabled; private Func<(bool shouldRun, bool shouldReceiveInput)>? _externalOnBeforeRunEmulatorOneFrame; private Action? _externalOnAfterRunEmulatorOneFrame; @@ -269,10 +270,7 @@ public virtual void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool sho public void RunEmulatorOneFrame() { // Process any UI actions that have been queued up from other threads - if (_externalControlEnabled) - { - ExternalControlProcessUIActions(); - } + ExternalControlProcessUIActions(); // Safety check to avoid running emulator if it's not in a running state. if (EmulatorState != EmulatorState.Running) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs index f4f332d2a..ca302626a 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs @@ -45,6 +45,7 @@ public interface IHostApp public bool ExternalControlDirectInvoke { get; } public Task ExternalControlInvokeOnUIThread(Func action); public void ExternalControlProcessUIActions(); + public bool ExternalControlEnabled { get; } public void EnableExternalControl( Func<(bool shouldRun, bool shouldReceiveInput)>? externalOnBeforeRunEmulatorOneFrame = null, Action? externalOnAfterRunEmulatorOneFrame = null); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs index 2db0244dd..d026ff532 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64BreakpointTool.cs @@ -12,56 +12,68 @@ public static class C64BreakpointTool [McpServerTool, Description("List all breakpoints in the C64 emulator.")] public static async Task ListBreakpoints(IHostApp hostApp, BreakpointManager breakpointManager) { - var breakpoints = breakpointManager.BreakPoints; - var result = new List(); - foreach (var bp in breakpoints) + try { - result.Add(new { Address = bp.Key, Enabled = bp.Value.Enabled }); + var breakpoints = breakpointManager.BreakPoints; + var result = new List(); + foreach (var bp in breakpoints) + { + result.Add(new { Address = bp.Key, Enabled = bp.Value.Enabled }); + } + return C64ToolHelper.BuildCallToolDataResult(result); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); } - return C64ToolHelper.BuildCallToolDataResult(result); } [McpServerTool, Description("Add a breakpoint at the specified address.")] public static async Task AddBreakpoint(IHostApp hostApp, BreakpointManager breakpointManager, ushort address) { - var breakpoints = breakpointManager.BreakPoints; - if (!breakpoints.ContainsKey(address)) - breakpoints.Add(address, new BreakPoint { Enabled = true }); - else - breakpoints[address].Enabled = true; - - EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return new CallToolResult(); + try + { + var breakpoints = breakpointManager.BreakPoints; + if (!breakpoints.ContainsKey(address)) + breakpoints.Add(address, new BreakPoint { Enabled = true }); + else + breakpoints[address].Enabled = true; + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Remove a breakpoint at the specified address.")] public static async Task RemoveBreakpoint(IHostApp hostApp, BreakpointManager breakpointManager, ushort address) { - var breakpoints = breakpointManager.BreakPoints; - if (breakpoints.ContainsKey(address)) - breakpoints.Remove(address); - - EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return new CallToolResult(); + try + { + var breakpoints = breakpointManager.BreakPoints; + if (breakpoints.ContainsKey(address)) + breakpoints.Remove(address); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } } [McpServerTool, Description("Remove all breakpoints.")] public static async Task RemoveAllBreakpoints(IHostApp hostApp, BreakpointManager breakpointManager) { - var breakpoints = breakpointManager.BreakPoints; - breakpoints.Clear(); - EnableOrDisableBreakpointHandling(hostApp, breakpointManager); - return new CallToolResult(); - } - - private static void EnableOrDisableBreakpointHandling(IHostApp hostApp, BreakpointManager breakpointManager) - { - if (hostApp.CurrentSystemRunner == null) - return; // No system runner available, nothing to do. - - if (breakpointManager.BreakPoints.Count == 0) - breakpointManager.DisableBreakpointHandling(hostApp); - else - breakpointManager.EnableBreakpointHandling(hostApp); + try + { + var breakpoints = breakpointManager.BreakPoints; + breakpoints.Clear(); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } } -} +} \ No newline at end of file diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs index c8411eb9c..4ba2ac977 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs @@ -15,9 +15,10 @@ public static async Task GetState(IHostApp hostApp) try { + C64ToolHelper.AssertEmulatorIsC64(hostApp); + await hostApp.ExternalControlInvokeOnUIThread(async () => { - C64ToolHelper.AssertEmulatorIsC64(hostApp); emulatorState = hostApp.EmulatorState; }); @@ -31,16 +32,18 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Starts C64 emulator.")] - public static async Task Start(IHostApp hostApp) + public static async Task Start(IHostApp hostApp, StateManager stateManager) { try { + C64ToolHelper.AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - C64ToolHelper.AssertC64EmulatorIsPausedOrUninitialzied(hostApp); await hostApp.Start(); }); + stateManager.EnableMCPControl(hostApp); return new CallToolResult(); } catch (Exception ex) @@ -51,14 +54,16 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Pause C64 emulator")] - public static async Task Pause(IHostApp hostApp) + public static async Task Pause(IHostApp hostApp, StateManager stateManager) { try { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); hostApp.Pause(); }); return new CallToolResult(); @@ -70,14 +75,16 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Stop C64 emulator")] - public static async Task Stop(IHostApp hostApp) + public static async Task Stop(IHostApp hostApp, StateManager stateManager) { try { + C64ToolHelper.AssertC64EmulatorIsRunningOrPaused(hostApp); + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - C64ToolHelper.AssertC64EmulatorIsRunningOrPaused(hostApp); hostApp.Stop(); }); return new CallToolResult(); @@ -89,21 +96,22 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfSeconds(IHostApp hostApp, int numberOfSeconds) + public static async Task RunNumberOfSeconds(IHostApp hostApp, StateManager stateManager, int numberOfSeconds) { try { + if (numberOfSeconds <= 0) + throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - if (numberOfSeconds <= 0) - throw new ArgumentException("Number of seconds must be greater than zero.", nameof(numberOfSeconds)); - - C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); var c64 = C64ToolHelper.GetC64(hostApp); //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); - await RunNumberOfFrames(hostApp, numberOfFrames); + await RunNumberOfFrames(hostApp, stateManager, numberOfFrames); }); return new CallToolResult(); @@ -115,18 +123,18 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] - public static async Task RunNumberOfFrames(IHostApp hostApp, int numberOfFrames) + public static async Task RunNumberOfFrames(IHostApp hostApp, StateManager stateManager, int numberOfFrames) { try { + if (numberOfFrames <= 0) + throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - if (numberOfFrames <= 0) - throw new ArgumentException("Frame count must be greater than zero.", nameof(numberOfFrames)); - - C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); - for (int i = 0; i < numberOfFrames; i++) hostApp.RunEmulatorOneFrame(); @@ -140,17 +148,18 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } [McpServerTool, Description("Runs the C64 emulator for specified number of instructions")] - public static async Task RunNumberOfInstructions(IHostApp hostApp, int numberOfInstructions) + public static async Task RunNumberOfInstructions(IHostApp hostApp, StateManager stateManager, int numberOfInstructions) { try { + if (numberOfInstructions <= 0) + throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - if (numberOfInstructions <= 0) - throw new ArgumentException("Instruction count must be greater than zero.", nameof(numberOfInstructions)); - - C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); for (int i = 0; i < numberOfInstructions; i++) hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); @@ -163,4 +172,105 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => return C64ToolHelper.BuildCallToolErrorResult(ex); } } + + + [McpServerTool, Description("Checks if C64 emulator has MCP control enabled.")] + public static async Task IsMCPControlEnabled(IHostApp hostApp, StateManager stateManager) + { + try + { + return C64ToolHelper.BuildCallToolDataResult(stateManager.IsMCPControlEnabled(hostApp)); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Enable MCP control of C64 emulator.")] + public static async Task EnableMCPControl(IHostApp hostApp, StateManager stateManager) + { + try + { + stateManager.EnableMCPControl(hostApp); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + + [McpServerTool, Description("Disable MCP control of C64 emulator.")] + public static async Task DisableMCPControl(IHostApp hostApp, StateManager stateManager) + { + try + { + stateManager.DisableMCPControl(hostApp); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Check if the CPU in the C64 emulator is paused.")] + public static async Task IsCPUPaused(IHostApp hostApp, StateManager stateManager) + { + try + { + return C64ToolHelper.BuildCallToolDataResult(stateManager.IsMCPControlEnabled(hostApp) && stateManager.CpuExecutionPaused); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Pause CPU execution in the C64 emulator.")] + public static async Task PauseCPU(IHostApp hostApp, StateManager stateManager) + { + try + { + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + stateManager.PauseCPUExecution(); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool, Description("Resume CPU execution in the C64 emulator.")] + public static async Task ResumeCPU(IHostApp hostApp, StateManager stateManager) + { + try + { + C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + stateManager.ResumeCPUExecution(); + return new CallToolResult(); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + // [McpServerTool, Description("Run CPU in the C64 emulator. Until the next breakpoint or for a max number of frames")] + // public static async Task RunCPU(IHostApp hostApp, StateManager stateManager, int maxNumberOfFrames) + // { + // try + // { + // C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + // hostApp.RunEmulatorOneFrame(); + // return new CallToolResult(); + // } + // catch (Exception ex) + // { + // return C64ToolHelper.BuildCallToolErrorResult(ex); + // } + // } } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs index f1587c70a..9fc1fbaaf 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs @@ -62,6 +62,12 @@ public static C64 GetC64(IHostApp hostApp) throw new InvalidOperationException("Current running system is not a C64 instance."); } + public static void AssertMCPControlEnabled(IHostApp hostApp, StateManager stateManager) + { + if (!stateManager.IsMCPControlEnabled(hostApp)) + throw new DotNet6502Exception("MCP control is not enabled. Please enable it first."); + } + public static CallToolResult BuildCallToolDataResult(object data) { // NOTE: Doesn't seem to work with returning StructuredContent, it's always empty. diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs index 1188107b0..cccbf2db8 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/BreakpointManager.cs @@ -1,32 +1,9 @@ -using Highbyte.DotNet6502; using Highbyte.DotNet6502.Monitor; -using Highbyte.DotNet6502.Systems; public class BreakpointManager { private readonly Dictionary _breakPoints = new(); public Dictionary BreakPoints => _breakPoints; - private IExecEvaluator? _originalExecEvaluator; - - private bool _cpuExecutionPaused = false; - public bool CpuExecutionPaused => _cpuExecutionPaused; - - public void EnableBreakpointHandling(IHostApp hostApp) - { - if (hostApp.CurrentSystemRunner == null) - throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); - _originalExecEvaluator = hostApp.CurrentSystemRunner.CustomExecEvaluator; - hostApp.CurrentSystemRunner.SetCustomExecEvaluator(new BreakPointExecEvaluator(_breakPoints)); - hostApp.EnableExternalControl(OnBeforeRunEmulatorOneFrame, OnAfterRunEmulatorOneFrame); - } - - public void DisableBreakpointHandling(IHostApp hostApp) - { - if (hostApp.CurrentSystemRunner == null) - throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); - hostApp.CurrentSystemRunner.SetCustomExecEvaluator(_originalExecEvaluator); - hostApp.DisableExternalControl(); - } public void AddBreakpoint(ushort address) { @@ -46,33 +23,4 @@ public void RemoveAllBreakpoints() { _breakPoints.Clear(); } - - public void PauseCPUExecution() - { - _cpuExecutionPaused = true; - } - - public void ContinueCPUExecution() - { - _cpuExecutionPaused = false; - } - - private (bool shouldRun, bool shouldReceiveInput) OnBeforeRunEmulatorOneFrame() - { - // If CPU execution is paused, we should not run the emulator. - return (!_cpuExecutionPaused, true); - } - - private void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) - { - if (execEvaluatorTriggerResult.Triggered) - { - // If a breakpoint was hit, pause the CPU execution. - PauseCPUExecution(); - if (execEvaluatorTriggerResult.TriggerType == ExecEvaluatorTriggerReasonType.DebugBreakPoint) - { - //Console.WriteLine($"Breakpoint hit at address: {execEvaluatorTriggerResult.TriggerDescription}"); - } - } - } } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs new file mode 100644 index 000000000..2ab37edff --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs @@ -0,0 +1,75 @@ +using Highbyte.DotNet6502; +using Highbyte.DotNet6502.Monitor; +using Highbyte.DotNet6502.Systems; + +public class StateManager +{ + private IExecEvaluator? _originalExecEvaluator; + + private bool _cpuExecutionPaused = false; + private readonly BreakpointManager _breakPointManager; + + public bool CpuExecutionPaused => _cpuExecutionPaused; + + public StateManager(BreakpointManager breakpointManager) + { + _breakPointManager = breakpointManager; + } + + public bool IsMCPControlEnabled(IHostApp hostApp) + { + return hostApp.ExternalControlEnabled; + } + + public void EnableMCPControl(IHostApp hostApp) + { + if (hostApp.CurrentSystemRunner == null) + throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); + if (IsMCPControlEnabled(hostApp)) + throw new DotNet6502Exception("MCP control is already enabled in the host app."); + + hostApp.EnableExternalControl(OnBeforeRunEmulatorOneFrame, OnAfterRunEmulatorOneFrame); + _originalExecEvaluator = hostApp.CurrentSystemRunner.CustomExecEvaluator; + hostApp.CurrentSystemRunner.SetCustomExecEvaluator(new BreakPointExecEvaluator(_breakPointManager.BreakPoints)); + } + + public void DisableMCPControl(IHostApp hostApp) + { + if (hostApp.CurrentSystemRunner == null) + throw new DotNet6502Exception("CurrentSystemRunner is not set in the host app."); + if (!IsMCPControlEnabled(hostApp)) + throw new DotNet6502Exception("MCP control is not enabled in the host app."); + + hostApp.DisableExternalControl(); + hostApp.CurrentSystemRunner.SetCustomExecEvaluator(_originalExecEvaluator); + } + + public void PauseCPUExecution() + { + _cpuExecutionPaused = true; + } + + public void ResumeCPUExecution() + { + _cpuExecutionPaused = false; + } + + private (bool shouldRun, bool shouldReceiveInput) OnBeforeRunEmulatorOneFrame() + { + // If CPU execution is paused, we should not run the emulator. + return (!_cpuExecutionPaused, true); + } + + private void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) + { + if (execEvaluatorTriggerResult.Triggered) + { + // If a breakpoint was hit, pause the CPU execution. + PauseCPUExecution(); + if (execEvaluatorTriggerResult.TriggerType == ExecEvaluatorTriggerReasonType.DebugBreakPoint) + { + //Console.WriteLine($"Breakpoint hit at address: {execEvaluatorTriggerResult.TriggerDescription}"); + } + } + } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index ec3e57233..3c1b072c1 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -16,6 +16,7 @@ public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilde // Add the host app as a singleton service builder.Services.AddSingleton((sp) => hostApp); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Logging.AddConsole(consoleLogOptions => { From bec2216d5a6cea8b1317653dd74b7cb6deb4984a Mon Sep 17 00:00:00 2001 From: Highbyte Date: Thu, 26 Jun 2025 00:15:04 +0200 Subject: [PATCH 12/17] More MCP fixes --- .vscode/mcp.json | 18 +- .../Highbyte.DotNet6502.Systems/HostApp.cs | 8 +- .../Highbyte.DotNet6502.Systems/IHostApp.cs | 2 +- .../SystemRunner.cs | 6 + .../C64StateTool.cs | 192 +++++++++--------- .../Contract/ExecutionResult.cs | 9 + .../Services/StateManager.cs | 12 +- 7 files changed, 134 insertions(+), 113 deletions(-) create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/ExecutionResult.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 26b68ffb0..e5a139103 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -14,17 +14,17 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" // } - // "DotNet6502-SadConsole-Windows": { - // "type": "stdio", - // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", - // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" - // } - - "DotNet6502-SadConsole-Mac": { + "DotNet6502-SadConsole-Windows": { "type": "stdio", - "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0/Highbyte.DotNet6502.App.SadConsole", - "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0" + "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" } + + // "DotNet6502-SadConsole-Mac": { + // "type": "stdio", + // "command": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0/Highbyte.DotNet6502.App.SadConsole", + // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/apps/Highbyte.DotNet6502.App.SadConsole/bin/Debug/net9.0" + // } //"DotNet6502-SilkNet-Windows": { // "type": "stdio", diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index fdc64f359..f734f5d43 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -267,14 +267,14 @@ public virtual void OnBeforeRunEmulatorOneFrame(out bool shouldRun, out bool sho shouldRun = true; shouldReceiveInput = true; } - public void RunEmulatorOneFrame() + public ExecEvaluatorTriggerResult RunEmulatorOneFrame() { // Process any UI actions that have been queued up from other threads ExternalControlProcessUIActions(); // Safety check to avoid running emulator if it's not in a running state. if (EmulatorState != EmulatorState.Running) - return; + return ExecEvaluatorTriggerResult.NotTriggered; bool shouldRun = false; bool shouldReceiveInput = false; @@ -289,7 +289,7 @@ public void RunEmulatorOneFrame() } if (!shouldRun) - return; + return ExecEvaluatorTriggerResult.NotTriggered; _updateFps.Update(); @@ -313,7 +313,9 @@ public void RunEmulatorOneFrame() } _systemTime!.Stop(); + return execEvaluatorTriggerResult; } + public virtual void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluatorTriggerResult) { } public virtual void OnBeforeDrawFrame(bool emulatorWillBeRendered) { } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs index ca302626a..b7ccc3362 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/IHostApp.cs @@ -23,7 +23,7 @@ public interface IHostApp public Task Reset(); public void Close(); - public void RunEmulatorOneFrame(); + public ExecEvaluatorTriggerResult RunEmulatorOneFrame(); public void DrawFrame(); public Task IsSystemConfigValid(); diff --git a/src/libraries/Highbyte.DotNet6502.Systems/SystemRunner.cs b/src/libraries/Highbyte.DotNet6502.Systems/SystemRunner.cs index 8219aaa6a..0324b6bf7 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/SystemRunner.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/SystemRunner.cs @@ -91,6 +91,12 @@ public ExecEvaluatorTriggerResult RunEmulatorOneFrame() return execEvaluatorTriggerResult; } + public ExecEvaluatorTriggerResult RunEmulatorOneInstruction() + { + var execEvaluatorTriggerResult = _system.ExecuteOneInstruction(this, out _, _customExecEvaluator); + return execEvaluatorTriggerResult; + } + /// /// Called by host app that runs the emulator, once per frame tied to the host app rendering frequency. /// diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs index 4ba2ac977..a27b03b53 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs @@ -1,5 +1,8 @@ using System.ComponentModel; using Highbyte.DotNet6502.Systems; +using Highbyte.DotNet6502.Systems.Commodore64; +using Highbyte.DotNet6502.Util.MCPServer.Contract; +using Highbyte.DotNet6502.Utils; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; @@ -9,20 +12,25 @@ namespace Highbyte.DotNet6502.Util.MCPServer; public static class C64StateTool { [McpServerTool, Description("Get C64 emulator state (Uninitialized, Running, Paused)")] - public static async Task GetState(IHostApp hostApp) + public static async Task GetState(IHostApp hostApp, StateManager stateManager) { EmulatorState emulatorState = default; - + bool isMCPControlEnabled = false; try { C64ToolHelper.AssertEmulatorIsC64(hostApp); - await hostApp.ExternalControlInvokeOnUIThread(async () => { emulatorState = hostApp.EmulatorState; + isMCPControlEnabled = stateManager.IsMCPControlEnabled(hostApp); }); - - return C64ToolHelper.BuildCallToolDataResult(emulatorState); + return C64ToolHelper.BuildCallToolDataResult( + new + { + emulatorState = emulatorState, + isMCPControlEnabled = stateManager.IsMCPControlEnabled(hostApp), + IsCPUPaused = stateManager.IsCpuExecutionPaused + }); } catch (Exception ex) @@ -43,7 +51,6 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => { await hostApp.Start(); }); - stateManager.EnableMCPControl(hostApp); return new CallToolResult(); } catch (Exception ex) @@ -53,27 +60,6 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } - [McpServerTool, Description("Pause C64 emulator")] - public static async Task Pause(IHostApp hostApp, StateManager stateManager) - { - try - { - C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); - C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); - - // Safest to run code that uses objects the emulator uses on the UI thread. - await hostApp.ExternalControlInvokeOnUIThread(async () => - { - hostApp.Pause(); - }); - return new CallToolResult(); - } - catch (Exception ex) - { - return C64ToolHelper.BuildCallToolErrorResult(ex); - } - } - [McpServerTool, Description("Stop C64 emulator")] public static async Task Stop(IHostApp hostApp, StateManager stateManager) { @@ -95,7 +81,7 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => } } - [McpServerTool, Description("Runs the C64 emulator for specified number of frames")] + [McpServerTool, Description("Runs the C64 emulator for specified number of seconds")] public static async Task RunNumberOfSeconds(IHostApp hostApp, StateManager stateManager, int numberOfSeconds) { try @@ -105,16 +91,33 @@ public static async Task RunNumberOfSeconds(IHostApp hostApp, St C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + ExecutionResult executionResult = new ExecutionResult + { + ExecutionPauseWasTriggered = false, + ExecutionPauseReason = null + }; + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { var c64 = C64ToolHelper.GetC64(hostApp); //var numberOfFrames = numberOfSeconds * c64.Vic2.Vic2Model.?? int numberOfFrames = (int)(numberOfSeconds * c64.Screen.RefreshFrequencyHz); - await RunNumberOfFrames(hostApp, stateManager, numberOfFrames); - + for (int i = 0; i < numberOfFrames; i++) + { + var execEvaluatorTriggerResult = hostApp.RunEmulatorOneFrame(); + if (execEvaluatorTriggerResult.Triggered) + { + executionResult.ExecutionPauseWasTriggered = execEvaluatorTriggerResult.Triggered; + executionResult.ExecutionPauseReason = execEvaluatorTriggerResult.TriggerType; + break; + } + } + + executionResult.NextInstruction = OutputGen.GetNextInstructionDisassembly(c64.CPU, c64.Mem); }); - return new CallToolResult(); + + return C64ToolHelper.BuildCallToolDataResult(executionResult); } catch (Exception ex) { @@ -132,14 +135,31 @@ public static async Task RunNumberOfFrames(IHostApp hostApp, Sta C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + ExecutionResult executionResult = new ExecutionResult + { + ExecutionPauseWasTriggered = false, + ExecutionPauseReason = null + }; + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { for (int i = 0; i < numberOfFrames; i++) - hostApp.RunEmulatorOneFrame(); + { + var execEvaluatorTriggerResult = hostApp.RunEmulatorOneFrame(); + if (execEvaluatorTriggerResult.Triggered) + { + executionResult.ExecutionPauseWasTriggered = execEvaluatorTriggerResult.Triggered; + executionResult.ExecutionPauseReason = execEvaluatorTriggerResult.TriggerType; + break; + } + } + var c64 = C64ToolHelper.GetC64(hostApp); + executionResult.NextInstruction = OutputGen.GetNextInstructionDisassembly(c64.CPU, c64.Mem); }); - return new CallToolResult(); + + return C64ToolHelper.BuildCallToolDataResult(executionResult); } catch (Exception ex) { @@ -157,15 +177,30 @@ public static async Task RunNumberOfInstructions(IHostApp hostAp C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); + ExecutionResult executionResult = new ExecutionResult + { + ExecutionPauseWasTriggered = false, + ExecutionPauseReason = null + }; + // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { - for (int i = 0; i < numberOfInstructions; i++) - hostApp.CurrentRunningSystem.CPU.ExecuteOneInstruction(hostApp.CurrentRunningSystem.Mem); + { + var execEvaluatorTriggerResult = hostApp.CurrentSystemRunner.RunEmulatorOneInstruction(); + if (execEvaluatorTriggerResult.Triggered) + { + executionResult.ExecutionPauseWasTriggered = execEvaluatorTriggerResult.Triggered; + executionResult.ExecutionPauseReason = execEvaluatorTriggerResult.TriggerType; + break; + } + } + var c64 = C64ToolHelper.GetC64(hostApp); + executionResult.NextInstruction = OutputGen.GetNextInstructionDisassembly(c64.CPU, c64.Mem); }); - return new CallToolResult(); + return C64ToolHelper.BuildCallToolDataResult(executionResult); } catch (Exception ex) { @@ -193,35 +228,21 @@ public static async Task EnableMCPControl(IHostApp hostApp, Stat try { stateManager.EnableMCPControl(hostApp); - return new CallToolResult(); - } - catch (Exception ex) - { - return C64ToolHelper.BuildCallToolErrorResult(ex); - } - } + ExecutionResult executionResult = new ExecutionResult + { + ExecutionPauseWasTriggered = false, + ExecutionPauseReason = null, + }; - [McpServerTool, Description("Disable MCP control of C64 emulator.")] - public static async Task DisableMCPControl(IHostApp hostApp, StateManager stateManager) - { - try - { - stateManager.DisableMCPControl(hostApp); - return new CallToolResult(); - } - catch (Exception ex) - { - return C64ToolHelper.BuildCallToolErrorResult(ex); - } - } + // Safest to run code that uses objects the emulator uses on the UI thread. + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + var c64 = C64ToolHelper.GetC64(hostApp); + executionResult.NextInstruction = OutputGen.GetNextInstructionDisassembly(c64.CPU, c64.Mem); + }); - [McpServerTool, Description("Check if the CPU in the C64 emulator is paused.")] - public static async Task IsCPUPaused(IHostApp hostApp, StateManager stateManager) - { - try - { - return C64ToolHelper.BuildCallToolDataResult(stateManager.IsMCPControlEnabled(hostApp) && stateManager.CpuExecutionPaused); + return C64ToolHelper.BuildCallToolDataResult(executionResult); } catch (Exception ex) { @@ -229,28 +250,13 @@ public static async Task IsCPUPaused(IHostApp hostApp, StateMana } } - [McpServerTool, Description("Pause CPU execution in the C64 emulator.")] - public static async Task PauseCPU(IHostApp hostApp, StateManager stateManager) - { - try - { - C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); - stateManager.PauseCPUExecution(); - return new CallToolResult(); - } - catch (Exception ex) - { - return C64ToolHelper.BuildCallToolErrorResult(ex); - } - } - [McpServerTool, Description("Resume CPU execution in the C64 emulator.")] - public static async Task ResumeCPU(IHostApp hostApp, StateManager stateManager) + [McpServerTool, Description("Disable MCP control of C64 emulator.")] + public static async Task DisableMCPControl(IHostApp hostApp, StateManager stateManager) { try { - C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); - stateManager.ResumeCPUExecution(); + stateManager.DisableMCPControl(hostApp); return new CallToolResult(); } catch (Exception ex) @@ -259,18 +265,16 @@ public static async Task ResumeCPU(IHostApp hostApp, StateManage } } - // [McpServerTool, Description("Run CPU in the C64 emulator. Until the next breakpoint or for a max number of frames")] - // public static async Task RunCPU(IHostApp hostApp, StateManager stateManager, int maxNumberOfFrames) - // { - // try - // { - // C64ToolHelper.AssertMCPControlEnabled(hostApp, stateManager); - // hostApp.RunEmulatorOneFrame(); - // return new CallToolResult(); - // } - // catch (Exception ex) - // { - // return C64ToolHelper.BuildCallToolErrorResult(ex); - // } - // } + //[McpServerTool, Description("Check if the CPU in the C64 emulator is paused.")] + //public static async Task IsCPUPaused(IHostApp hostApp, StateManager stateManager) + //{ + // try + // { + // return C64ToolHelper.BuildCallToolDataResult(stateManager.IsMCPControlEnabled(hostApp) && stateManager.IsCpuExecutionPaused); + // } + // catch (Exception ex) + // { + // return C64ToolHelper.BuildCallToolErrorResult(ex); + // } + //} } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/ExecutionResult.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/ExecutionResult.cs new file mode 100644 index 000000000..a7b1a26af --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Contract/ExecutionResult.cs @@ -0,0 +1,9 @@ + +namespace Highbyte.DotNet6502.Util.MCPServer.Contract; +public class ExecutionResult +{ + public CPURegisters CPURegisters { get; set; } + public bool ExecutionPauseWasTriggered { get; set; } + public ExecEvaluatorTriggerReasonType? ExecutionPauseReason { get; set; } + public string NextInstruction { get; set; } +} diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs index 2ab37edff..434484954 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Services/StateManager.cs @@ -9,7 +9,7 @@ public class StateManager private bool _cpuExecutionPaused = false; private readonly BreakpointManager _breakPointManager; - public bool CpuExecutionPaused => _cpuExecutionPaused; + public bool IsCpuExecutionPaused => _cpuExecutionPaused; public StateManager(BreakpointManager breakpointManager) { @@ -28,6 +28,8 @@ public void EnableMCPControl(IHostApp hostApp) if (IsMCPControlEnabled(hostApp)) throw new DotNet6502Exception("MCP control is already enabled in the host app."); + PauseCPUExecution(); + hostApp.EnableExternalControl(OnBeforeRunEmulatorOneFrame, OnAfterRunEmulatorOneFrame); _originalExecEvaluator = hostApp.CurrentSystemRunner.CustomExecEvaluator; hostApp.CurrentSystemRunner.SetCustomExecEvaluator(new BreakPointExecEvaluator(_breakPointManager.BreakPoints)); @@ -42,14 +44,15 @@ public void DisableMCPControl(IHostApp hostApp) hostApp.DisableExternalControl(); hostApp.CurrentSystemRunner.SetCustomExecEvaluator(_originalExecEvaluator); + ResumeCPUExecution(); } - public void PauseCPUExecution() + private void PauseCPUExecution() { _cpuExecutionPaused = true; } - public void ResumeCPUExecution() + private void ResumeCPUExecution() { _cpuExecutionPaused = false; } @@ -64,11 +67,8 @@ private void OnAfterRunEmulatorOneFrame(ExecEvaluatorTriggerResult execEvaluator { if (execEvaluatorTriggerResult.Triggered) { - // If a breakpoint was hit, pause the CPU execution. - PauseCPUExecution(); if (execEvaluatorTriggerResult.TriggerType == ExecEvaluatorTriggerReasonType.DebugBreakPoint) { - //Console.WriteLine($"Breakpoint hit at address: {execEvaluatorTriggerResult.TriggerDescription}"); } } } From 0ce09e8713a4bdc40577513e88752011e364885d Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 27 Jun 2025 10:58:43 +0200 Subject: [PATCH 13/17] Minor fixes --- .../Highbyte.DotNet6502.Systems/HostApp.cs | 27 ++++--- .../Program.cs | 72 +++++++++---------- .../ToolSetup.cs | 18 ++--- 3 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index f734f5d43..a2d73c6f7 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -409,24 +409,31 @@ private void InitInstrumentation(ISystem system) private readonly ConcurrentQueue _externalControlUIActions = new(); public virtual bool ExternalControlDirectInvoke { get; } = false; - public Task ExternalControlInvokeOnUIThread(Func action) + public Task ExternalControlInvokeOnUIThread(Func action) { + var tcs = new TaskCompletionSource(); + // If HostApp is running on same thread as the external control code, execute the action directly. if (ExternalControlDirectInvoke) { - try - { - action(); - return Task.CompletedTask; - } - catch (Exception ex) + Task.Run(async () => { - return Task.FromException(ex); - } + try + { + await action(); + tcs.SetResult(null); + //return Task.CompletedTask; + } + catch (Exception ex) + { + tcs.SetException(ex); + //return Task.FromException(ex); + } + return tcs.Task; + }); } // Otherwise, enqueue the action to be executed on the UI thread (see ExternalControlProcessUIActions); - var tcs = new TaskCompletionSource(); _externalControlUIActions.Enqueue(async () => { try diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs index b1c104ce3..af12d2952 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs @@ -1,6 +1,7 @@ using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Logging.InMem; +using Highbyte.DotNet6502.Util.MCPServer; using Highbyte.DotNet6502.Util.MCPServer.Emulator; using Highbyte.DotNet6502.Util.MCPServer.Emulator.SystemSetup; using Microsoft.Extensions.Configuration; @@ -19,51 +20,50 @@ .AddJsonFile("appsettings.json") .AddJsonFile("appsettings.Development.json", optional: true); -builder.Services - .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(); - // ---------- // Register the emulator EmbeddedMCPHostApp as singleton service // ---------- var configuration = builder.Configuration; -builder.Services.AddSingleton((sp) => + +// ---------- +// Create emulator logging +// ---------- +DotNet6502InMemLogStore logStore = new() { WriteDebugMessage = true }; +var logConfig = new DotNet6502InMemLoggerConfiguration(logStore); +var loggerFactory = LoggerFactory.Create(builder => { - // ---------- - // Create emulator logging - // ---------- - DotNet6502InMemLogStore logStore = new() { WriteDebugMessage = true }; - var logConfig = new DotNet6502InMemLoggerConfiguration(logStore); - var loggerFactory = LoggerFactory.Create(builder => - { - logConfig.LogLevel = LogLevel.Information; // LogLevel.Debug, LogLevel.Information, - builder.AddInMem(logConfig); - builder.SetMinimumLevel(LogLevel.Trace); - }); + logConfig.LogLevel = LogLevel.Information; // LogLevel.Debug, LogLevel.Information, + builder.AddInMem(logConfig); + builder.SetMinimumLevel(LogLevel.Trace); +}); - // ---------- - // Get emulator host config - // ---------- - var emulatorConfig = new EmulatorConfig(); - configuration.GetSection(EmulatorConfig.ConfigSectionName).Bind(emulatorConfig); +// ---------- +// Get emulator host config +// ---------- +var emulatorConfig = new EmulatorConfig(); +configuration.GetSection(EmulatorConfig.ConfigSectionName).Bind(emulatorConfig); - // ---------- - // Get systems - // ---------- - var systemList = new SystemList(); - var c64Setup = new C64Setup(loggerFactory, configuration); - systemList.AddSystem(c64Setup); +// ---------- +// Get systems +// ---------- +var systemList = new SystemList(); +var c64Setup = new C64Setup(loggerFactory, configuration); +systemList.AddSystem(c64Setup); - // ---------- - // Create & init emulator host app - // ---------- - var embeddedMCPHostApp = new EmbeddedMCPHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, configuration); - embeddedMCPHostApp.Init(); - embeddedMCPHostApp.SelectSystem(C64.SystemName).Wait(); +// ---------- +// Create & init emulator host app +// ---------- +var embeddedMCPHostApp = new EmbeddedMCPHostApp(systemList, loggerFactory, emulatorConfig, logStore, logConfig, configuration); +embeddedMCPHostApp.Init(); +embeddedMCPHostApp.SelectSystem(C64.SystemName).Wait(); - return embeddedMCPHostApp; -}); +// Automatically enable external control of the emulator. As the EmbeddedMCPHostApp won't run the emulator by itself on another thread, the only way we control it via MCP server commands. +embeddedMCPHostApp.EnableExternalControl(); + +// ---------- +// Configure MCP server and tools +// ---------- +builder.ConfigureDotNet6502McpServerTools(embeddedMCPHostApp); await builder.Build().RunAsync(); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index 3c1b072c1..a9dfb231b 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -10,29 +10,29 @@ public static class ToolSetup { public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilder builder, IHostApp hostApp, Assembly? additionalToolsAssembly = null) { - // Add MCP server tools from the specified assembly - builder.Services.AddMcpServer() - .WithToolsFromAssembly(additionalToolsAssembly); - - // Add the host app as a singleton service + // DI: Register the emulator host app builder.Services.AddSingleton((sp) => hostApp); + + // DI: Register MCP server dependencies builder.Services.AddSingleton(); builder.Services.AddSingleton(); + + // Add console logging builder.Logging.AddConsole(consoleLogOptions => { // Configure all console logs to go to stderr to no interfere with with MCP server STDIO communication consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; }); + // Register MCP server and tools var mcpServerBuilder = builder.Services .AddMcpServer() .WithStdioServerTransport(); + + // Register MCP tools mcpServerBuilder.WithToolsFromAssembly(typeof(C64StateTool).Assembly); + // Add additional MCP server tools from the specified assembly if (additionalToolsAssembly != null) - { mcpServerBuilder.WithToolsFromAssembly(additionalToolsAssembly); - } - - builder.Services.AddSingleton((sp) => hostApp); } } From 3d85d8e9df1aa8cb2b27332888fec26e3ad03997 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 27 Jun 2025 12:31:49 +0200 Subject: [PATCH 14/17] Minor fixes --- .vscode/launch.json | 17 ++++++++++++-- .vscode/mcp.json | 23 +++++++++++++++---- .vscode/tasks.json | 2 +- .../Highbyte.DotNet6502.Systems/HostApp.cs | 2 +- .../Program.cs | 5 +--- .../ToolSetup.cs | 20 ++++++++++++++-- 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 74e6daa47..7e9bbff30 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -50,7 +50,20 @@ // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "externalTerminal", "stopAtEntry": false - }, + }, + { + "name": "MCP server console app - .NET Core Launch", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build mcp server", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/utils/Highbyte.DotNet6502.Util.MCPServer/bin/Debug/net9.0/Highbyte.DotNet6502.Util.MCPServer.dll", + "args": [], + "cwd": "${workspaceFolder}/src/utils/Highbyte.DotNet6502.Util.MCPServer/bin/Debug/net9.0/", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "externalTerminal", + "stopAtEntry": false + }, { "name": ".NET Core Attach", "type": "coreclr", @@ -58,7 +71,7 @@ "processId": "${command:pickProcess}" }, { - "name": ".NET Core Attach MCPServer", + "name": ".NET Core Attach MCPServer (Windows)", "type": "coreclr", "request": "attach", "processName": "Highbyte.DotNet6502.Util.MCPServer.exe" diff --git a/.vscode/mcp.json b/.vscode/mcp.json index e5a139103..779eadcdd 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -14,11 +14,24 @@ // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" // } - "DotNet6502-SadConsole-Windows": { - "type": "stdio", - "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", - "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" - } + "DotNet6502-Embedded-Mac": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj", + "--configuration", + "Debug" + ], + "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer" + } + + // "DotNet6502-SadConsole-Windows": { + // "type": "stdio", + // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" + // } // "DotNet6502-SadConsole-Mac": { // "type": "stdio", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4a6d34e2e..7c3de8f57 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -120,7 +120,7 @@ { "label": "build mcp server", "command": "dotnet", - "type": "attach", + "type": "process", "args": [ "build", "${workspaceFolder}/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj", diff --git a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs index a2d73c6f7..6b0246da9 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/HostApp.cs @@ -429,8 +429,8 @@ public Task ExternalControlInvokeOnUIThread(Func action) tcs.SetException(ex); //return Task.FromException(ex); } - return tcs.Task; }); + return tcs.Task; } // Otherwise, enqueue the action to be executed on the UI thread (see ExternalControlProcessUIActions); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs index af12d2952..c2a1ee226 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/Program.cs @@ -58,12 +58,9 @@ embeddedMCPHostApp.Init(); embeddedMCPHostApp.SelectSystem(C64.SystemName).Wait(); -// Automatically enable external control of the emulator. As the EmbeddedMCPHostApp won't run the emulator by itself on another thread, the only way we control it via MCP server commands. -embeddedMCPHostApp.EnableExternalControl(); - // ---------- // Configure MCP server and tools // ---------- -builder.ConfigureDotNet6502McpServerTools(embeddedMCPHostApp); +builder.ConfigureDotNet6502McpServerTools(embeddedMCPHostApp, mcpControlEnabledFromStart: true); await builder.Build().RunAsync(); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index a9dfb231b..0bb127d9b 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -8,13 +8,29 @@ namespace Highbyte.DotNet6502.Util.MCPServer; public static class ToolSetup { - public static void ConfigureDotNet6502McpServerTools(this IHostApplicationBuilder builder, IHostApp hostApp, Assembly? additionalToolsAssembly = null) + public static void ConfigureDotNet6502McpServerTools( + this IHostApplicationBuilder builder, + IHostApp hostApp, + Assembly? additionalToolsAssembly = null, + bool mcpControlEnabledFromStart = false) { // DI: Register the emulator host app builder.Services.AddSingleton((sp) => hostApp); // DI: Register MCP server dependencies - builder.Services.AddSingleton(); + builder.Services.AddSingleton((sp) => + { + + var breakpointManager = sp.GetRequiredService(); + var stateManger = new StateManager(breakpointManager); + if (mcpControlEnabledFromStart) + { + // Automatically start and enable external control of the emulator. + hostApp.Start(); + stateManger.EnableMCPControl(hostApp); + } + return stateManger; + }); builder.Services.AddSingleton(); // Add console logging From fa3b7b137735f3ed1955b04780ce422292095323 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 30 Jun 2025 17:42:00 +0200 Subject: [PATCH 15/17] Add readme --- .vscode/mcp.json | 34 ++++++++----------- .../README_MCP.md | 24 +++++++++++++ 2 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 src/utils/Highbyte.DotNet6502.Util.MCPServer/README_MCP.md diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 779eadcdd..b9b4ce302 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,31 +1,27 @@ { "inputs": [], "servers": { - // "DotNet6502-Embedded-Windows": { - // "type": "stdio", - // "command": "dotnet", - // "args": [ - // "run", - // "--project", - // "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj", - // "--configuration", - // "Debug" - // ], - // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" - // } - - "DotNet6502-Embedded-Mac": { + "DotNet6502-Embedded-Windows": { "type": "stdio", "command": "dotnet", "args": [ "run", "--project", - "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj", - "--configuration", - "Debug" + "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj" ], - "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer" - } + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" + } + + // "DotNet6502-Embedded-Mac": { + // "type": "stdio", + // "command": "dotnet", + // "args": [ + // "run", + // "--project", + // "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer/Highbyte.DotNet6502.Util.MCPServer.csproj" + // ], + // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer" + // } // "DotNet6502-SadConsole-Windows": { // "type": "stdio", diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/README_MCP.md b/src/utils/Highbyte.DotNet6502.Util.MCPServer/README_MCP.md new file mode 100644 index 000000000..d5f91e525 --- /dev/null +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/README_MCP.md @@ -0,0 +1,24 @@ +# MCP server commands +The MCP server communicates over STDIO with JSON. + +To troubleshoot the JSON commands, start the project `Highbyte.DotNet6502.Util.MCPServer.csproj` and paste the JSON commands in the terminal and press enter. + +## List tools +```json +{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1} +``` + + +## Examples: state handling + +```json +{"jsonrpc":"2.0","method":"tools/call","params":{"name":"GetState","args":{}},"id":2} +``` + +```json +{"jsonrpc":"2.0","method":"tools/call","params":{"name":"Start","args":{}},"id":2} +``` + +```json +{"jsonrpc":"2.0","method":"tools/call","params":{"name":"GetCPURegisters","args":{}},"id":2} +``` From c22d5aac28da5f051c52815e7c4a1a79d6ee5cec Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 30 Jun 2025 18:45:14 +0200 Subject: [PATCH 16/17] Minor fixes --- .../C64MemoryTool.cs | 23 ++++++++++++------- .../C64StateTool.cs | 3 ++- .../C64ToolHelper.cs | 2 +- .../ToolSetup.cs | 13 ++++++++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs index d9e47de16..e8b557f85 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs @@ -88,19 +88,26 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => /// /// /// Array of bytes to write to memory> - [McpServerTool, Description("Writes a range of values (byte array) starting at specified memory address in C64 emulator. Expects 'values' as an array of integers.")] - public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, byte[] values) + [McpServerTool, Description("Writes a range of numeric values (array) starting at specified memory address in C64 emulator.")] + public static async Task WriteMemoryRange(IHostApp hostApp, ushort address, int[] values) { try { // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => { + // Convert values to byte array + if (values == null || values.Length == 0) + throw new ArgumentException("Values cannot be null or empty.", nameof(values)); + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); var c64 = C64ToolHelper.GetC64(hostApp); - if (address + values.Length > c64.Mem.Size) - values = values.Take(c64.Mem.Size - address).ToArray(); - c64.Mem.StoreData(address, values); + + var valuesAsBytes = values.Select(v => (byte)(v & 0xFF)).ToArray(); // Ensure values are within byte range + + if (address + valuesAsBytes.Length > c64.Mem.Size) + valuesAsBytes = valuesAsBytes.Take(c64.Mem.Size - address).ToArray(); + c64.Mem.StoreData(address, valuesAsBytes); }); return new CallToolResult(); } @@ -115,7 +122,7 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => /// /// /// Sequence of 8-bit bytes in hex separated by space to write to memory> - [McpServerTool, Description("Writes a range of values starting at specified memory address in C64 emulator.")] + [McpServerTool, Description("Writes a range of hex values separated by space starting at specified memory address in C64 emulator.")] public static async Task WriteMemoryRangeAsHexString(IHostApp hostApp, ushort address, string hexValues) { try @@ -126,9 +133,9 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => // Convert hex string to byte array if (string.IsNullOrWhiteSpace(hexValues)) throw new ArgumentException("Hex values cannot be null or empty.", nameof(hexValues)); - var values = hexValues + int[] values = hexValues .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries) - .Select(hex => Convert.ToByte(hex, 16)) + .Select(hex => Convert.ToInt32(hex, 16)) .ToArray(); await WriteMemoryRange(hostApp, address, values); diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs index a27b03b53..7207ceb24 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64StateTool.cs @@ -44,7 +44,8 @@ public static async Task Start(IHostApp hostApp, StateManager st { try { - C64ToolHelper.AssertC64EmulatorIsPausedOrUninitialzied(hostApp); + if (hostApp.EmulatorState == EmulatorState.Running) + return C64ToolHelper.BuildCallToolDataResult("C64 emulator is already running."); // Safest to run code that uses objects the emulator uses on the UI thread. await hostApp.ExternalControlInvokeOnUIThread(async () => diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs index 9fc1fbaaf..1100a401e 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64ToolHelper.cs @@ -32,7 +32,7 @@ public static void AssertC64EmulatorIsPausedOrUninitialzied(IHostApp hostApp) AssertEmulatorIsC64(hostApp); if (hostApp.EmulatorState != EmulatorState.Paused && hostApp.EmulatorState != EmulatorState.Uninitialized) { - throw new InvalidOperationException($"C64 emulator not paused uninitialzied. Current state: {hostApp.EmulatorState}"); + throw new InvalidOperationException($"C64 emulator not paused or uninitialzied. Current state: {hostApp.EmulatorState}"); } } diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs index 0bb127d9b..3bb301bc2 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/ToolSetup.cs @@ -15,7 +15,15 @@ public static void ConfigureDotNet6502McpServerTools( bool mcpControlEnabledFromStart = false) { // DI: Register the emulator host app - builder.Services.AddSingleton((sp) => hostApp); + builder.Services.AddSingleton((sp) => { + + if (mcpControlEnabledFromStart) + { + // Automatically start the emulator. + hostApp.Start(); + } + return hostApp; + }); // DI: Register MCP server dependencies builder.Services.AddSingleton((sp) => @@ -25,8 +33,7 @@ public static void ConfigureDotNet6502McpServerTools( var stateManger = new StateManager(breakpointManager); if (mcpControlEnabledFromStart) { - // Automatically start and enable external control of the emulator. - hostApp.Start(); + // Enable external control of the emulator. stateManger.EnableMCPControl(hostApp); } return stateManger; From 33418dd49ecdc4e6e047671c444fcf41c52f4dba Mon Sep 17 00:00:00 2001 From: Highbyte Date: Mon, 30 Jun 2025 19:23:25 +0200 Subject: [PATCH 17/17] Add additional memory tools --- .vscode/mcp.json | 30 ++++---- .../C64MemoryTool.cs | 68 +++++++++++++++++++ 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index b9b4ce302..41ca54bfd 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,16 +1,16 @@ { "inputs": [], "servers": { - "DotNet6502-Embedded-Windows": { - "type": "stdio", - "command": "dotnet", - "args": [ - "run", - "--project", - "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj" - ], - "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" - } + // "DotNet6502-Embedded-Windows": { + // "type": "stdio", + // "command": "dotnet", + // "args": [ + // "run", + // "--project", + // "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer\\Highbyte.DotNet6502.Util.MCPServer.csproj" + // ], + // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\utils\\Highbyte.DotNet6502.Util.MCPServer" + // } // "DotNet6502-Embedded-Mac": { // "type": "stdio", @@ -23,11 +23,11 @@ // "cwd": "/Users/highbyte/source/repos/dotnet-6502/src/utils/Highbyte.DotNet6502.Util.MCPServer" // } - // "DotNet6502-SadConsole-Windows": { - // "type": "stdio", - // "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", - // "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" - // } + "DotNet6502-SadConsole-Windows": { + "type": "stdio", + "command": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0\\Highbyte.DotNet6502.App.SadConsole.exe", + "cwd": "C:\\Users\\highb\\source\\repos\\dotnet-6502\\src\\apps\\Highbyte.DotNet6502.App.SadConsole\\bin\\Debug\\net9.0" + } // "DotNet6502-SadConsole-Mac": { // "type": "stdio", diff --git a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs index e8b557f85..fdc98b739 100644 --- a/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs +++ b/src/utils/Highbyte.DotNet6502.Util.MCPServer/C64MemoryTool.cs @@ -147,4 +147,72 @@ await hostApp.ExternalControlInvokeOnUIThread(async () => return C64ToolHelper.BuildCallToolErrorResult(ex); } } + + [McpServerTool(UseStructuredContent = true, ReadOnly = true), Description("Reads current C64 screen memory and converts screen codes to ASCII characters.")] + public static async Task ReadScreenMemoryAsAscii(IHostApp hostApp) + { + try + { + string asciiScreen = string.Empty; + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + // C64 screen memory: $0400-$07E7 (1000 bytes, 40x25) + const ushort screenMemStart = 0x0400; + const int screenMemLen = 1000; + var screenCodes = c64.Mem.ReadData(screenMemStart, screenMemLen); + // Use existing conversion: screen code -> petscii -> ascii + var asciiSb = new System.Text.StringBuilder(screenMemLen); + foreach (var screenCode in screenCodes) + { + var petscii = Highbyte.DotNet6502.Systems.Commodore64.Video.Petscii.C64ScreenCodeToPetscII(screenCode); + var ascii = Highbyte.DotNet6502.Systems.Commodore64.Video.Petscii.PetscIIToAscII(petscii); + asciiSb.Append((char)ascii); + } + asciiScreen = asciiSb.ToString(); + }); + return C64ToolHelper.BuildCallToolDataResult(asciiScreen); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } + + [McpServerTool(UseStructuredContent = true, ReadOnly = true), Description("Disassembles a memory range in the C64 emulator and returns the disassembly as a list of strings.")] + public static async Task DisassembleMemoryRange(IHostApp hostApp, ushort startAddress, ushort endAddress) + { + try + { + List disassembly = new(); + await hostApp.ExternalControlInvokeOnUIThread(async () => + { + C64ToolHelper.AssertC64EmulatorIsRunning(hostApp); + var c64 = C64ToolHelper.GetC64(hostApp); + var cpu = c64.CPU; + var mem = c64.Mem; + + ushort currentAddress = startAddress; + bool cont = true; + while (cont) + { + // Use OutputGen to get disassembly for current instruction + var line = Highbyte.DotNet6502.Utils.OutputGen.GetInstructionDisassembly(cpu, mem, currentAddress); + disassembly.Add(line); + // Get address of next instruction + var nextInstructionAddress = cpu.GetNextInstructionAddress(mem, currentAddress); + if (nextInstructionAddress > endAddress || (currentAddress >= nextInstructionAddress)) + cont = false; + else + currentAddress = nextInstructionAddress; + } + }); + return C64ToolHelper.BuildCallToolDataResult(disassembly); + } + catch (Exception ex) + { + return C64ToolHelper.BuildCallToolErrorResult(ex); + } + } }