diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/VGA/Data/RegisterValueSet.cs b/src/Spice86.Core/Emulator/InterruptHandlers/VGA/Data/RegisterValueSet.cs index 1294604ba0..4070054477 100644 --- a/src/Spice86.Core/Emulator/InterruptHandlers/VGA/Data/RegisterValueSet.cs +++ b/src/Spice86.Core/Emulator/InterruptHandlers/VGA/Data/RegisterValueSet.cs @@ -54,6 +54,7 @@ internal readonly struct RegisterValueSet { [0x11] = new VideoMode(new VgaMode(MemoryModel.Planar, 640, 480, 1, 8, 16, VgaConstants.GraphicsSegment), 0xFF, Palettes.Ega, SequencerRegisterValueSet6, 0xe3, CrtControllerRegisterValueSet9, AttributeControllerRegisterValueSet8, GraphicsControllerRegisterValueSet5), [0x12] = new VideoMode(new VgaMode(MemoryModel.Planar, 640, 480, 4, 8, 16, VgaConstants.GraphicsSegment), 0xFF, Palettes.Ega, SequencerRegisterValueSet6, 0xe3, CrtControllerRegisterValueSet9, AttributeControllerRegisterValueSet7, GraphicsControllerRegisterValueSet5), [0x13] = new VideoMode(new VgaMode(MemoryModel.Packed, 320, 200, 8, 8, 8, VgaConstants.GraphicsSegment), 0xFF, Palettes.Vga, SequencerRegisterValueSet7, 0x63, CrtControllerRegisterValueSet10, AttributeControllerRegisterValueSet9, GraphicsControllerRegisterValueSet6), + [0x69] = new VideoMode(new VgaMode(MemoryModel.Planar, 640, 480, 4, 8, 16, VgaConstants.GraphicsSegment), 0xFF, Palettes.Ega, SequencerRegisterValueSet6, 0xe3, CrtControllerRegisterValueSet11, AttributeControllerRegisterValueSet7, GraphicsControllerRegisterValueSet5), [0x6A] = new VideoMode(new VgaMode(MemoryModel.Planar, 800, 600, 4, 8, 16, VgaConstants.GraphicsSegment), 0xFF, Palettes.Ega, SequencerRegisterValueSet6, 0xe3, CrtControllerRegisterValueSet11, AttributeControllerRegisterValueSet7, GraphicsControllerRegisterValueSet5) }; } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Mcp/McpHttpHost.cs b/src/Spice86.Core/Emulator/Mcp/McpHttpHost.cs index 2e4ec78385..4a5c5352a2 100644 --- a/src/Spice86.Core/Emulator/Mcp/McpHttpHost.cs +++ b/src/Spice86.Core/Emulator/Mcp/McpHttpHost.cs @@ -56,10 +56,11 @@ public void Start(EmulatorMcpServices services, int port = 8081, }; }) .WithHttpTransport(options => { - // Stateless mode: the server never issues Mcp-Session-Id headers. - // Stateful sessions cause 404 when AI clients reuse a session ID - // from a fresh TCP connection or skip the notifications/initialized - // handshake step, which is a common real-world pattern. + // Stateless mode keeps things simple — no session management, + // no Mcp-Session-Id headers, compatible with AI clients. + // The SDK does not map GET /mcp in stateless mode (POST only), + // but we add our own GET handler below to satisfy clients + // (like opencode remote MCP) that probe with GET first. options.Stateless = true; }) .WithToolsFromAssembly(typeof(EmulatorMcpTools).Assembly); @@ -70,10 +71,7 @@ public void Start(EmulatorMcpServices services, int port = 8081, } } - builder.WebHost.ConfigureKestrel(kestrel => { - kestrel.ListenLocalhost(port); - }); - + builder.WebHost.UseUrls($"http://localhost:{port}"); _app = builder.Build(); _app.MapGet("/health", () => Results.Json(new { status = "ok", @@ -81,6 +79,19 @@ public void Start(EmulatorMcpServices services, int port = 8081, })); _app.MapMcp("/mcp"); + // The SDK doesn't map GET /mcp in stateless mode, but some MCP clients + // (including opencode remote MCP) probe with GET first. Return a minimal + // SSE endpoint event telling the client to POST to the same URL. + _app.MapGet("/mcp", async (HttpContext context) => + { + context.Response.Headers.ContentType = "text/event-stream"; + context.Response.Headers.CacheControl = "no-cache,no-store"; + context.Response.Headers["X-Accel-Buffering"] = "no"; + await context.Response.WriteAsync( + $"event: endpoint\ndata: /mcp/\n\n"); + await context.Response.Body.FlushAsync(); + }); + _serverThread = new Thread(RunServerLoop) { Name = "McpHttpHost", IsBackground = true @@ -95,12 +106,20 @@ private void RunServerLoop() { } try { - _app.Run(); + _app.StartAsync().GetAwaiter().GetResult(); + _loggerService.Information("MCP HTTP server is now listening"); + // Block this thread indefinitely so the server keeps running. + // Using _app.Run() instead caused premature shutdown on background + // threads because ConsoleLifetime has no console to watch on a + // non-main thread. } catch (ObjectDisposedException) { // Host disposed while thread was exiting. - } catch (InvalidOperationException ex) { - _loggerService.Error(ex, "MCP HTTP server stopped unexpectedly"); + } catch (Exception ex) { + _loggerService.Error(ex, "MCP HTTP server crashed"); } + + // Wait forever so the thread never exits. + new System.Threading.ManualResetEvent(false).WaitOne(); } public void Dispose() { diff --git a/src/Spice86.Core/Emulator/OperatingSystem/DosDriveManager.cs b/src/Spice86.Core/Emulator/OperatingSystem/DosDriveManager.cs index c387e723dd..13a0ffb9f9 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/DosDriveManager.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/DosDriveManager.cs @@ -48,8 +48,8 @@ public DosDriveManager(ILoggerService loggerService, string? cDriveFolderPath, s cDriveFolderPath = DosPathResolver.GetExeParentFolder(executablePath); } cDriveFolderPath = ConvertUtils.ToSlashFolderPath(cDriveFolderPath); - _driveMap[GetDriveIndex('A')] = new VirtualDrive() { DriveLetter = 'A', MountedHostDirectory = "" }; - _driveMap[GetDriveIndex('B')] = new VirtualDrive() { DriveLetter = 'B', MountedHostDirectory = "" }; + _driveMap[GetDriveIndex('A')] = new VirtualDrive() { DriveLetter = 'A', MountedHostDirectory = cDriveFolderPath }; + _driveMap[GetDriveIndex('B')] = new VirtualDrive() { DriveLetter = 'B', MountedHostDirectory = cDriveFolderPath }; var cDrive = new VirtualDrive { DriveLetter = 'C', MountedHostDirectory = cDriveFolderPath }; _driveMap[GetDriveIndex('C')] = cDrive; CurrentDrive = cDrive; diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosDriveBase.cs b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosDriveBase.cs index cdd170f7c8..d1540080d8 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosDriveBase.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosDriveBase.cs @@ -32,7 +32,7 @@ public abstract class DosDriveBase { /// /// Gets the absolute path to the current DOS directory in use on the drive. /// - public string CurrentDosDirectory { get; set; } = ""; + public string CurrentDosDirectory { get; set; } = string.Empty; /// /// Gets if it is a network drive. Not supported, always