// McpServer.cpp: implementation of the embedded BKEmu MCP server.
//
// NOTE: the precompiled header is disabled for this translation unit (see the
// project file). That lets us include the third-party headers in the right
// order: httplib / nlohmann-json pull in <winsock2.h> and use std::min/std::max
// and therefore must be seen BEFORE MFC drags in <windows.h> (which otherwise
// defines the legacy <winsock.h> and the min/max macros).

// --- third-party first, in this exact order -------------------------------
#include "3rdparty/httplib.h"   // brings in <winsock2.h> before <windows.h>
#include "3rdparty/json.hpp"
// --- then the MFC / emulator world ----------------------------------------
#include "pch.h"
#include <atlimage.h>           // CImage (GDI+ PNG encoding for the screen tool)
#include "Platform.h"           // CPlatform g_Platform, CCPU, CPUREG, PSW_BIT, CScreen
#include "FDDStd326.h"          // CFDDStd326 (disk_attach / disk_detach)
#include "BreakpointView.h"     // g_pPaneBreakpointView (live refresh of the BP pane)
#include "McpServer.h"
#include "McpMarshal.h"

#include <functional>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>

using json = nlohmann::json;

namespace
{
	constexpr char  MCP_PROTOCOL_VERSION[] = "2024-11-05";
	constexpr char  MCP_SERVER_NAME[]      = "BKEmu";
	constexpr char  MCP_SERVER_VERSION[]   = "0.1";

	// Formats a 16-bit value as a 6-digit octal string (BK convention).
	std::string Oct(uint16_t v)
	{
		char buf[8];
		std::snprintf(buf, sizeof(buf), "%06o", static_cast<unsigned>(v));
		return std::string(buf);
	}

	// { "oct": "001234", "dec": 668 } for a register-style value.
	json RegVal(uint16_t v)
	{
		return json{ { "oct", Oct(v) }, { "dec", static_cast<int>(v) } };
	}

	// Converts a (wide, in this Unicode build) CString to a UTF-8 std::string
	// suitable for embedding in JSON.
	std::string ToUtf8(const CString &s)
	{
		if (s.IsEmpty())
		{
			return {};
		}

		const int wlen = s.GetLength();
		const int len  = ::WideCharToMultiByte(CP_UTF8, 0, s, wlen, nullptr, 0, nullptr, nullptr);

		std::string out(static_cast<size_t>(len), '\0');
		::WideCharToMultiByte(CP_UTF8, 0, s, wlen, out.data(), len, nullptr, nullptr);
		return out;
	}

	std::string ToUtf8(const std::wstring &s)
	{
		if (s.empty())
		{
			return {};
		}

		const int wlen = s.length();
		const int len = ::WideCharToMultiByte(CP_UTF8, 0, s.c_str(), wlen, nullptr, 0, nullptr, nullptr);

		std::string out(static_cast<size_t>(len), '\0');
		::WideCharToMultiByte(CP_UTF8, 0, s.c_str(), wlen, out.data(), len, nullptr, nullptr);
		return out;
	}

	// The debugger embeds inline syntax-highlighting tags (COLORED_TAG followed
	// by one color-index digit) in disassembly text. Strip them to plain text.
	std::string PlainText(const CString &s)
	{
		CString   out;
		const int n = s.GetLength();
		int       i = 0;

		while (i < n)
		{
			if (i + COLORED_TAG_LENGTH < n && CString(s.Mid(i, COLORED_TAG_LENGTH)) == _T(COLORED_TAG))
			{
				i += COLORED_TAG_LENGTH + 1;   // skip "<C>" + the color digit
			}
			else
			{
				out += s[i++];
			}
		}

		out.TrimRight();
		return ToUtf8(out);
	}

	// Parses a numeric argument that may be a JSON integer (decimal) or a string.
	// Strings are octal by default (BK convention); a leading 0x/0X means hex.
	// The result is masked to 16 bits. Throws std::runtime_error on bad input.
	uint16_t ParseNumArg(const json &v, const char *what)
	{
		try
		{
			if (v.is_number_integer())
			{
				return static_cast<uint16_t>(v.get<long long>() & 0xFFFF);
			}

			if (v.is_string())
			{
				std::string s = v.get<std::string>();
				const size_t b = s.find_first_not_of(" \t");
				const size_t e = s.find_last_not_of(" \t");

				if (b != std::string::npos)
				{
					s = s.substr(b, e - b + 1);

					int base = 8;

					if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
					{
						base = 16;
					}

					size_t pos = 0;
					const unsigned long val = std::stoul(s, &pos, base);

					if (pos == s.size())
					{
						return static_cast<uint16_t>(val & 0xFFFF);
					}
				}
			}
		}
		catch (...)
		{
			// fall through to the throw below
		}

		throw std::runtime_error(std::string("invalid ") + what + " value");
	}

	// Reads an optional boolean argument, throwing on a wrong (non-bool) type.
	bool OptBool(const json &args, const char *key, bool def)
	{
		if (!args.contains(key) || args[key].is_null())
		{
			return def;
		}

		if (!args[key].is_boolean())
		{
			throw std::runtime_error(std::string("invalid ") + key + " (expected boolean)");
		}

		return args[key].get<bool>();
	}

	// Converts a UTF-8 std::string (as carried in JSON) to a filesystem path.
	// On Windows fs::path is wide, and constructing it from a narrow string would
	// use the ANSI code page, so we go through UTF-8 -> UTF-16 explicitly to keep
	// non-ASCII (e.g. Cyrillic) paths correct.
	fs::path Utf8ToPath(const std::string &s)
	{
		if (s.empty())
		{
			return {};
		}

		const int n = ::MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
		std::wstring w(static_cast<size_t>(n), L'\0');
		::MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), w.data(), n);
		return fs::path(w);
	}

	// Standard base64 encoder (for the screen tool's PNG bytes).
	std::string Base64(const uint8_t *data, size_t len)
	{
		static const char tbl[] =
		    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

		std::string out;
		out.reserve(((len + 2) / 3) * 4);

		size_t i = 0;

		for (; i + 3 <= len; i += 3)
		{
			const uint32_t n = (static_cast<uint32_t>(data[i]) << 16) |
			                   (static_cast<uint32_t>(data[i + 1]) << 8) |
			                   static_cast<uint32_t>(data[i + 2]);
			out.push_back(tbl[(n >> 18) & 63]);
			out.push_back(tbl[(n >> 12) & 63]);
			out.push_back(tbl[(n >> 6) & 63]);
			out.push_back(tbl[n & 63]);
		}

		if (i < len)
		{
			const bool     two = (i + 1 < len);
			const uint32_t n   = (static_cast<uint32_t>(data[i]) << 16) |
			                     (two ? (static_cast<uint32_t>(data[i + 1]) << 8) : 0);
			out.push_back(tbl[(n >> 18) & 63]);
			out.push_back(tbl[(n >> 12) & 63]);
			out.push_back(two ? tbl[(n >> 6) & 63] : '=');
			out.push_back('=');
		}

		return out;
	}
}

//////////////////////////////////////////////////////////////////////////////
// Tool registry
//////////////////////////////////////////////////////////////////////////////

namespace
{
	// A tool handler returns the tool's structured result as JSON, or throws
	// std::exception (its message is surfaced to the model as an error result).
	struct McpTool
	{
		std::string                          name;
		std::string                          description;
		json                                 inputSchema;
		std::function<json(const json &args)> handler;
	};

	// --- cpu_state ---------------------------------------------------------
	// Reads registers, PSW flags, run state and the last executed instruction.
	json Tool_CpuState(const json & /*args*/)
	{
		json    state;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CCPU *pCPU = g_Platform.GetCPUPtr();

			if (!pCPU)
			{
				err = "CPU is not available (no board created)";
				return;
			}

			state["running"] = g_Platform.IsCPURun();
			state["breaked"] = g_Platform.IsCPUBreaked();

			json regs;
			regs["r0"] = RegVal(pCPU->GetRON(CPUREG::R0));
			regs["r1"] = RegVal(pCPU->GetRON(CPUREG::R1));
			regs["r2"] = RegVal(pCPU->GetRON(CPUREG::R2));
			regs["r3"] = RegVal(pCPU->GetRON(CPUREG::R3));
			regs["r4"] = RegVal(pCPU->GetRON(CPUREG::R4));
			regs["r5"] = RegVal(pCPU->GetRON(CPUREG::R5));
			regs["sp"] = RegVal(pCPU->GetRON(CPUREG::SP));
			regs["pc"] = RegVal(pCPU->GetRON(CPUREG::PC));
			state["registers"] = regs;

			const uint16_t psw = pCPU->GetPSW();
			state["psw"] = RegVal(psw);
			state["psw_flags"] =
			{
				{ "C", !!(psw & (1 << PSW_BIT::C)) },
				{ "V", !!(psw & (1 << PSW_BIT::V)) },
				{ "Z", !!(psw & (1 << PSW_BIT::Z)) },
				{ "N", !!(psw & (1 << PSW_BIT::N)) },
				{ "T", !!(psw & (1 << PSW_BIT::T)) },
				{ "P", !!(psw & (1 << PSW_BIT::P)) },
			};

			const DbgStackPoint_t ci = pCPU->GetCurrentInstruction();
			state["last_instruction"] =
			{
				{ "pc",    Oct(static_cast<uint16_t>(ci.pc)) },
				{ "instr", Oct(ci.instr) },
				{ "sp",    Oct(ci.sp_af) },
			};
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return state;
	}

	// --- sys_info ----------------------------------------------------------
	// Reports the MCP server, the emulated machine/configuration, CPU clock and
	// the set of attached peripherals. Useful for an agent to orient itself.
	json Tool_SysInfo(const json & /*args*/)
	{
		json info;

		// MCP server side: known statically / from the server object, no need to
		// touch emulator state.
		info["mcp"] =
		{
			{ "server",   MCP_SERVER_NAME },
			{ "version",  MCP_SERVER_VERSION },
			{ "protocol", MCP_PROTOCOL_VERSION },
			{ "port",     g_McpServer.GetPort() },
		};

		const bool ok = mcp::Run([&]
		{
			const int modelNum = static_cast<int>(g_Config.m_BKConfigModelNumber);

			info["machine"] =
			{
				{ "config_id",        ToUtf8(g_ConfigBKModelParameters.at(g_Config.m_BKConfigModelNumber).strBKModelConfigName) },
				{ "config_name",      ToUtf8(g_Config.GetBKConfName()) },
				{ "model_number",     modelNum },
				{ "board_model",      static_cast<int>(g_Config.GetBKBoardModel()) },
				{ "mpi_device_model", static_cast<int>(g_Config.GetMPIDeviceModel()) },
				{ "is_bk10",          g_Config.isBK10() },
				{ "is_bk11",          g_Config.isBK11() },
				{ "is_bk11m",         g_Config.isBK11M() },
				{ "board_present",    g_Platform.GetBoardPtr() != nullptr },
			};

			// Nominal clock for the family; GetCPUSpeed() is a UI helper that
			// returns 0 at base clock, so we derive the nominal value ourselves
			// and expose whether the CPU is currently overclocked.
			const int nominalHz = g_Config.isBK10() ? CPU_SPEED_BK10 : CPU_SPEED_BK11;

			info["cpu"] =
			{
				{ "running",              g_Platform.IsCPURun() },
				{ "breaked",              g_Platform.IsCPUBreaked() },
				{ "frequency_hz",         g_Platform.GetCPUFreq() },
				{ "nominal_frequency_hz", nominalHz },
				{ "overclocked",          g_Platform.GetCPUSpeed() != 0 },
				{ "freq_min_hz",          g_Platform.GetLowBound() },
				{ "freq_max_hz",          g_Platform.GetHighBound() },
			};

			const uint32_t pf = g_Platform.GetPlatformStatus();
			info["peripherals"] =
			{
				{ "bk11m_mode", !!(pf & CONFPLATFORM_11M) },
				{ "fdd",        !!(pf & CONFPLATFORM_FDD) },
				{ "hdd",        !!(pf & CONFPLATFORM_HDD) },
				{ "altpro",     !!(pf & CONFPLATFORM_ALTPRO) },
				{ "optok",      !!(pf & CONFPLATFORM_OPTOK) },
				{ "irps",       !!(pf & CONFPLATFORM_IRPS) },
				{ "azbk",       !!(pf & CONFPLATFORM_AZBK) },
			};
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		return info;
	}

	// Disassembles one instruction at pc into a JSON item and reports its length
	// in words via lenWords. Shared by the disasm and debug_break tools. Must be
	// called on the UI thread (uses the shared debugger object).
	json DisasmItem(CDebugger *pDbg, uint16_t pc, int &lenWords)
	{
		CDbgItemParam p;
		int           len = pDbg->DebugInstruction(pc, p);   // length in words

		if (len < 1)
		{
			len = 1;   // defensive: never stall
		}

		lenWords = len;

		json words = json::array();

		for (int w = 0; w < len && w < OPDATA_LEN; ++w)
		{
			words.push_back(Oct(p.opdata[w]));
		}

		return json
		{
			{ "addr",      Oct(pc) },
			{ "words",     words },
			{ "text",      PlainText(p.instr) },
			{ "len_words", len },
		};
	}

	// --- disasm ------------------------------------------------------------
	// Disassembles a run of instructions starting at an address (default: the
	// current PC). Memory is read without bus side effects.
	constexpr int DISASM_DEFAULT_COUNT = 16;
	constexpr int DISASM_MAX_COUNT     = 64;

	json Tool_Disasm(const json &args)
	{
		const bool hasAddr = args.contains("addr") && !args["addr"].is_null();
		const uint16_t reqAddr = hasAddr ? ParseNumArg(args["addr"], "addr") : 0;

		int count = DISASM_DEFAULT_COUNT;

		if (args.contains("count") && args["count"].is_number_integer())
		{
			count = args["count"].get<int>();
		}

		if (count < 1)
		{
			count = 1;
		}

		if (count > DISASM_MAX_COUNT)
		{
			count = DISASM_MAX_COUNT;
		}

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();
			CCPU      *pCPU = g_Platform.GetCPUPtr();

			if (!pDbg || !pCPU)
			{
				err = "debugger/CPU is not available (no board created)";
				return;
			}

			uint16_t pc = hasAddr ? reqAddr : pCPU->GetRON(CPUREG::PC);

			result["start"] = Oct(pc);

			json items = json::array();

			for (int n = 0; n < count; ++n)
			{
				int len = 1;
				items.push_back(DisasmItem(pDbg, pc, len));
				pc = static_cast<uint16_t>(pc + len * 2);   // wraps at 0177777
			}

			result["instructions"] = items;
			result["next"] = Oct(pc);   // address after the last instruction
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- mem_read ----------------------------------------------------------
	// Reads a range of bytes without bus side effects. Addresses are octal (BK
	// convention); byte values are returned as a compact hex string plus an ASCII
	// rendering (non-printable bytes shown as '.').
	constexpr int MEM_READ_DEFAULT_LEN = 64;
	constexpr int MEM_READ_MAX_LEN     = 4096;

	json Tool_MemRead(const json &args)
	{
		if (!args.contains("addr") || args["addr"].is_null())
		{
			throw std::runtime_error("addr is required");
		}

		const uint16_t addr = ParseNumArg(args["addr"], "addr");

		int len = MEM_READ_DEFAULT_LEN;

		if (args.contains("len") && !args["len"].is_null())
		{
			const json &L = args["len"];

			if (L.is_number_integer())
			{
				len = L.get<int>();
			}
			else if (L.is_string())
			{
				try
				{
					len = std::stoi(L.get<std::string>());   // a count: decimal
				}
				catch (...)
				{
					throw std::runtime_error("invalid len value");
				}
			}
			else
			{
				throw std::runtime_error("invalid len value");
			}
		}

		if (len < 1)
		{
			len = 1;
		}

		if (len > MEM_READ_MAX_LEN)
		{
			len = MEM_READ_MAX_LEN;
		}

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!g_Platform.GetBoardPtr() || !pDbg)
			{
				err = "no board created";
				return;
			}

			static const char hexd[] = "0123456789abcdef";
			std::string hex;
			std::string ascii;
			hex.reserve(static_cast<size_t>(len) * 2);
			ascii.reserve(static_cast<size_t>(len));

			for (int i = 0; i < len; ++i)
			{
				const uint8_t b = pDbg->GetDebugMemDumpByte(static_cast<uint16_t>(addr + i));   // wraps at 0177777
				hex.push_back(hexd[b >> 4]);
				hex.push_back(hexd[b & 0x0F]);
				ascii.push_back((b >= 0x20 && b < 0x7F) ? static_cast<char>(b) : '.');
			}

			result["addr"]  = Oct(addr);
			result["len"]   = len;
			result["end"]   = Oct(static_cast<uint16_t>(addr + len));   // first address past the range
			result["hex"]   = hex;
			result["ascii"] = ascii;
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- screen ------------------------------------------------------------
	// Capture the current display as a PNG and return it as an MCP image content
	// block (so the model can actually see it). Captures the screen exactly as
	// rendered now (current colour/BW/adaptive mode and palette) - no intrusive
	// mode switching; the mode is reported in the metadata.
	json Tool_Screen(const json &args)
	{
		std::string size = "native";

		if (args.contains("size") && args["size"].is_string())
		{
			size = args["size"].get<std::string>();
		}

		if (size != "native" && size != "view")
		{
			throw std::runtime_error("invalid size (expected native|view)");
		}

		std::string b64;
		json        meta;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CScreen *pScr = g_Platform.GetScreenPtr();

			if (!g_Platform.GetBoardPtr() || !pScr)
			{
				err = "no board created";
				return;
			}

			HBITMAP hbm = pScr->GetScreenshot();   // 32-bpp copy of the current texture

			if (!hbm)
			{
				err = "failed to capture screen";
				return;
			}

			if (size == "view")
			{
				// stretch to the current aspect-corrected viewport (deletes the original)
				const CPoint svw = pScr->GetScreenViewport();
				hbm = static_cast<HBITMAP>(CopyImage(hbm, IMAGE_BITMAP, svw.x, svw.y, LR_COPYDELETEORG));

				if (!hbm)
				{
					err = "failed to scale screen";
					return;
				}
			}

			CImage image;
			image.Attach(hbm, CImage::DIBOR_DEFAULT);   // CImage now owns and frees hbm
			const int w = image.GetWidth();
			const int h = image.GetHeight();

			IStream *pStream = nullptr;

			if (FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &pStream)) || !pStream)
			{
				err = "failed to create memory stream";
				return;
			}

			std::string raw;

			if (SUCCEEDED(image.Save(pStream, Gdiplus::ImageFormatPNG)))
			{
				HGLOBAL hg = nullptr;

				if (SUCCEEDED(GetHGlobalFromStream(pStream, &hg)) && hg)
				{
					const SIZE_T sz = ::GlobalSize(hg);
					const void  *p  = ::GlobalLock(hg);

					if (p)
					{
						raw.assign(static_cast<const char *>(p), sz);
						::GlobalUnlock(hg);
					}
				}
			}

			pStream->Release();

			if (raw.empty())
			{
				err = "failed to encode PNG";
				return;
			}

			b64  = Base64(reinterpret_cast<const uint8_t *>(raw.data()), raw.size());
			meta =
			{
				{ "width",   w },
				{ "height",  h },
				{ "color",   pScr->IsColorMode() },
				{ "adapt",   pScr->IsAdaptMode() },
				{ "palette", pScr->GetPalette() },
				{ "format",  "png" },
				{ "bytes",   static_cast<int>(raw.size()) },
			};
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		// Rich result: an image block (seen by the model) plus a text metadata
		// block. The "_content" key is recognised by the server (see tools/call).
		return json
		{
			{
				"_content", json::array(
				{
					json{ { "type", "image" }, { "data", b64 }, { "mimeType", "image/png" } },
					json{ { "type", "text" }, { "text", meta.dump(2) } },
				})
			}
		};
	}

	// --- emu_run -----------------------------------------------------------
	// Resume free-running execution: leave a debug break if paused, and start the
	// CPU if it was fully stopped. Mirrors the resume side of the UI Run/Break.
	json Tool_EmuRun(const json & /*args*/)
	{
		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CCPU *pCPU = g_Platform.GetCPUPtr();

			if (!g_Platform.GetBoardPtr() || !pCPU)
			{
				err = "no board created";
				return;
			}

			const bool wasRunning = g_Platform.IsCPURun();
			const bool wasBreaked = g_Platform.IsCPUBreaked();

			if (!wasRunning)
			{
				g_Platform.RunCPU();        // starts running and clears any break
			}
			else if (wasBreaked)
			{
				g_Platform.UnbreakCPU();    // already running, just leave the break
			}

			result["running"] = g_Platform.IsCPURun();
			result["breaked"] = g_Platform.IsCPUBreaked();
			result["changed"] = (!wasRunning) || wasBreaked;
			result["pc"]      = Oct(pCPU->GetRON(CPUREG::PC));
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- debug_break -------------------------------------------------------
	// Pause the CPU into the debugger at the current PC. Idempotent: if it is
	// already paused, nothing changes and changed=false.
	json Tool_DebugBreak(const json & /*args*/)
	{
		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CCPU      *pCPU = g_Platform.GetCPUPtr();
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!g_Platform.GetBoardPtr() || !pCPU || !pDbg)
			{
				err = "no board created";
				return;
			}

			const bool changed = g_Platform.BreakCPU();   // false if already breaked
			const uint16_t pc  = pCPU->GetRON(CPUREG::PC);

			result["running"] = g_Platform.IsCPURun();
			result["breaked"] = g_Platform.IsCPUBreaked();
			result["changed"] = changed;
			result["pc"]      = Oct(pc);

			int len = 1;   // the instruction at the PC where we stopped
			result["instruction"] = DisasmItem(pDbg, pc, len);
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- debug_step --------------------------------------------------------
	// Single-step the paused CPU. Modes:
	//   into - execute one instruction, entering calls/traps;
	//   over - execute one instruction, running subroutine calls to completion;
	//   out  - run until the current subroutine returns;
	//   skip - advance PC past the current instruction WITHOUT executing it.
	// For into/over/out the CPU is briefly resumed and re-breaks itself on the
	// emulation thread; we wait (off the UI thread) for that to happen.
	constexpr int STEP_TIMEOUT_MS = 5000;

	json Tool_DebugStep(const json &args)
	{
		std::string mode = "into";

		if (args.contains("mode") && args["mode"].is_string())
		{
			mode = args["mode"].get<std::string>();
		}

		if (mode != "into" && mode != "over" && mode != "out" && mode != "skip")
		{
			throw std::runtime_error("invalid mode (expected into|over|out|skip)");
		}

		json        result;
		std::string err;
		bool        async = false;   // true for into/over/out (CPU resumed then re-breaks)

		result["mode"] = mode;

		// Phase 1: check preconditions and trigger the step, on the UI thread.
		const bool ok1 = mcp::Run([&]
		{
			CCPU      *pCPU = g_Platform.GetCPUPtr();
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!g_Platform.GetBoardPtr() || !pCPU || !pDbg)
			{
				err = "no board created";
				return;
			}

			if (!g_Platform.IsCPUBreaked())
			{
				err = "CPU is not paused; call debug_break first";
				return;
			}

			if (mode == "skip")
			{
				g_Platform.SkipLine();   // synchronous: just advances PC, stays paused
				return;
			}

			if (!g_Platform.IsCPURun())
			{
				err = "CPU is stopped; cannot step";
				return;
			}

			if (mode == "into")
			{
				g_Platform.RunInto();
			}
			else if (mode == "over")
			{
				g_Platform.RunOver();
			}
			else // "out"
			{
				g_Platform.RunOut();
			}

			async = true;
		});

		if (!ok1)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		// Phase 2: for into/over/out, wait for the emulation thread to re-break.
		bool completed = true;

		if (async)
		{
			int waited = 0;

			while (!g_Platform.IsCPUBreaked() && waited < STEP_TIMEOUT_MS)
			{
				::Sleep(2);
				waited += 2;
			}

			completed = g_Platform.IsCPUBreaked();
		}

		// Phase 3: read the resulting state on the UI thread.
		const bool ok3 = mcp::Run([&]
		{
			CCPU      *pCPU = g_Platform.GetCPUPtr();
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!pCPU || !pDbg)
			{
				err = "no board created";
				return;
			}

			const uint16_t pc = pCPU->GetRON(CPUREG::PC);

			result["running"]   = g_Platform.IsCPURun();
			result["breaked"]   = g_Platform.IsCPUBreaked();
			result["completed"] = completed;   // false only if a step timed out still running
			result["pc"]        = Oct(pc);

			if (g_Platform.IsCPUBreaked())
			{
				int len = 1;
				result["instruction"] = DisasmItem(pDbg, pc, len);
			}
		});

		if (!ok3)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- emu_reset ---------------------------------------------------------
	// Reset the board, equivalent to the reset button. Always a cold reset of the
	// CPU (ResetBoard -> ResetCold) followed by a restart. Modifiers mirror the
	// hardware: 'su' (СУ/reset -> restart at 100000 on BK11) and 'long_reset'
	// (long reset, relevant to the A16M controller).
	//
	// (ResetPlatform is intentionally not exposed: it is the handler for the CPU
	// RESET instruction / init-time peripheral reset, not a user-facing reset.)
	json Tool_EmuReset(const json &args)
	{
		const bool su        = OptBool(args, "su", false);
		const bool longReset = OptBool(args, "long_reset", false);

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CCPU *pCPU = g_Platform.GetCPUPtr();

			if (!g_Platform.GetBoardPtr() || !pCPU)
			{
				err = "no board created";
				return;
			}

			if (!g_Platform.IsCPURun())
			{
				// ResetBoard is a no-op unless the CPU is running (breaked counts).
				err = "CPU is not running; reset has no effect";
				return;
			}

			bool lr = longReset;            // in/out param, cleared by ResetBoard
			g_Platform.ResetBoard(lr, su);

			result["reset"]      = true;
			result["su"]         = su;
			result["long_reset"] = longReset;
			result["running"]    = g_Platform.IsCPURun();
			result["breaked"]    = g_Platform.IsCPUBreaked();
			result["pc"]         = Oct(pCPU->GetRON(CPUREG::PC));
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- breakpoints -------------------------------------------------------
	// Renders one breakpoint as JSON.
	json BpToJson(const CBreakPoint &bp, int index)
	{
		json j;
		j["index"]   = index;
		j["addr"]    = Oct(bp.GetAddress());
		j["type"]    = bp.IsAddress() ? "exec" : "access";
		j["enabled"] = bp.IsEnable();
		j["breaked"] = bp.IsBreaked();

		if (bp.IsMemAccess())
		{
			const uint32_t m = bp.GetMemAccess();
			j["access"] =
			{
				{ "read",  !!(m & BP_MEMACCESS_RD) },
				{ "write", !!(m & BP_MEMACCESS_WR) },
				{ "byte",  !!(m & BP_MEMACCESS_BYTE) },
			};
		}

		return j;
	}

	// Parses the access flags for a memory-access breakpoint from an "access"
	// string of letters r/w/b (read / write / byte). Without 'b' only word-sized
	// accesses trigger; 'b' also catches byte-sized accesses. Default "rwb".
	uint32_t ParseAccess(const json &args)
	{
		std::string s = "rwb";

		if (args.contains("access") && !args["access"].is_null())
		{
			if (!args["access"].is_string())
			{
				throw std::runtime_error("invalid access (expected a string of r/w/b)");
			}

			s = args["access"].get<std::string>();
		}

		uint32_t mask = 0;

		for (char c : s)
		{
			switch (c)
			{
				case 'r': case 'R': mask |= BP_MEMACCESS_RD;   break;
				case 'w': case 'W': mask |= BP_MEMACCESS_WR;   break;
				case 'b': case 'B': mask |= BP_MEMACCESS_BYTE; break;
				case ' ': break;
				default: throw std::runtime_error("invalid access char (use r, w, b)");
			}
		}

		if (!(mask & (BP_MEMACCESS_RD | BP_MEMACCESS_WR)))
		{
			throw std::runtime_error("access must include at least 'r' or 'w'");
		}

		return mask;
	}

	// Refreshes the GUI breakpoint pane (call on the UI thread after a change).
	void RefreshBreakpointPane()
	{
		if (g_pPaneBreakpointView)
		{
			g_pPaneBreakpointView->SendMessage(WM_DBG_BREAKPOINTS_UPDATE);
		}
	}

	// --- bp_list -----------------------------------------------------------
	json Tool_BpList(const json & /*args*/)
	{
		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!pDbg)
			{
				err = "debugger is not available";
				return;
			}

			CBreakPointList *lst = pDbg->GetBreakpointListPtr();
			std::lock_guard<std::mutex> lk(pDbg->m_mutBptLst);

			json items = json::array();
			int  idx = 0;

			for (const auto &bp : *lst)
			{
				items.push_back(BpToJson(bp, idx++));
			}

			result["count"]       = idx;
			result["breakpoints"] = items;
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- bp_set ------------------------------------------------------------
	// Add an execution ('exec', default) or memory-access ('access') breakpoint.
	json Tool_BpSet(const json &args)
	{
		if (!args.contains("addr") || args["addr"].is_null())
		{
			throw std::runtime_error("addr is required");
		}

		const uint16_t addr = ParseNumArg(args["addr"], "addr");

		std::string type = "exec";

		if (args.contains("type") && args["type"].is_string())
		{
			type = args["type"].get<std::string>();
		}

		if (type != "exec" && type != "access")
		{
			throw std::runtime_error("invalid type (expected exec|access)");
		}

		const uint32_t accessMask = (type == "access") ? ParseAccess(args) : 0;

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!pDbg)
			{
				err = "debugger is not available";
				return;
			}

			const bool added = (type == "exec")
			                   ? pDbg->SetSimpleBreakpoint(addr)
			                   : pDbg->SetMemAccessBreakpoint(addr, accessMask);

			result["added"] = added;
			result["addr"]  = Oct(addr);
			result["type"]  = type;

			if (type == "access")
			{
				result["access"] =
				{
					{ "read",  !!(accessMask & BP_MEMACCESS_RD) },
					{ "write", !!(accessMask & BP_MEMACCESS_WR) },
					{ "byte",  !!(accessMask & BP_MEMACCESS_BYTE) },
				};
			}

			if (!added)
			{
				// SetMemAccessBreakpoint/SetSimpleBreakpoint refuse a duplicate of
				// the same type+address and leave the existing one untouched.
				result["note"] = "a breakpoint of this type already exists at addr; unchanged";
			}

			CBreakPointList *lst = pDbg->GetBreakpointListPtr();
			std::lock_guard<std::mutex> lk(pDbg->m_mutBptLst);
			result["count"] = static_cast<int>(lst->size());

			if (added)
			{
				RefreshBreakpointPane();
			}
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- bp_remove ---------------------------------------------------------
	// Removes every breakpoint (exec and access) at an address. (The debugger's
	// own RemoveBreakpoint only handles exec breakpoints, so we erase directly.)
	json Tool_BpRemove(const json &args)
	{
		if (!args.contains("addr") || args["addr"].is_null())
		{
			throw std::runtime_error("addr is required");
		}

		const uint16_t addr = ParseNumArg(args["addr"], "addr");

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!pDbg)
			{
				err = "debugger is not available";
				return;
			}

			CBreakPointList *lst = pDbg->GetBreakpointListPtr();
			int removed = 0;

			{
				std::lock_guard<std::mutex> lk(pDbg->m_mutBptLst);

				for (auto it = lst->begin(); it != lst->end();)
				{
					if (it->GetAddress() == addr)
					{
						it = lst->erase(it);
						++removed;
					}
					else
					{
						++it;
					}
				}

				result["count"] = static_cast<int>(lst->size());
			}

			result["removed"] = removed;
			result["addr"]    = Oct(addr);

			if (removed > 0)
			{
				RefreshBreakpointPane();
			}
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- bp_clear ----------------------------------------------------------
	json Tool_BpClear(const json & /*args*/)
	{
		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CDebugger *pDbg = g_Platform.GetDebuggerPtr();

			if (!pDbg)
			{
				err = "debugger is not available";
				return;
			}

			int previous = 0;
			{
				CBreakPointList *lst = pDbg->GetBreakpointListPtr();
				std::lock_guard<std::mutex> lk(pDbg->m_mutBptLst);
				previous = static_cast<int>(lst->size());
			}

			pDbg->ClearBreakpointList();   // locks internally
			RefreshBreakpointPane();

			result["cleared"] = previous;
			result["count"]   = 0;
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- state_save --------------------------------------------------------
	// Save a full emulator state image (MSF) to a file. The CPU is briefly
	// paused for a consistent snapshot (mirroring the UI), then restored.
	json Tool_StateSave(const json &args)
	{
		if (!args.contains("path") || !args["path"].is_string())
		{
			throw std::runtime_error("path (string) is required");
		}

		const std::string pstr = args["path"].get<std::string>();
		const fs::path     path = Utf8ToPath(pstr);

		// Validate the destination directory up front. SaveMemoryState opens the
		// file non-silently, so letting it fail would pop a modal GUI message box
		// (and block this call); we reject a bad directory here instead.
		if (path.has_parent_path())
		{
			std::error_code ec;

			if (!fs::is_directory(path.parent_path(), ec))
			{
				throw std::runtime_error("destination directory does not exist");
			}
		}

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			if (!g_Platform.GetBoardPtr())
			{
				err = "no board created";
				return;
			}

			const bool bBroke = g_Platform.BreakCPU();           // consistent snapshot
			const bool saved  = g_Platform.SaveMemoryState(path);

			if (bBroke)
			{
				g_Platform.UnbreakCPU();                         // resume if we paused it
			}

			if (!saved)
			{
				err = "failed to save state (check the path is writable)";
				return;
			}

			result["saved"] = true;
			result["path"]  = pstr;

			std::error_code ec;
			const auto sz = fs::file_size(path, ec);

			if (!ec)
			{
				result["bytes"] = static_cast<std::uint64_t>(sz);
			}
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- state_load --------------------------------------------------------
	// Load a full emulator state image (MSF) from a file. The file is validated
	// first (silently) so a bad file is reported as an error rather than popping a
	// GUI message box; loading then reconfigures the platform and resumes it.
	json Tool_StateLoad(const json &args)
	{
		if (!args.contains("path") || !args["path"].is_string())
		{
			throw std::runtime_error("path (string) is required");
		}

		const std::string pstr = args["path"].get<std::string>();
		const fs::path     path = Utf8ToPath(pstr);

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CMainFrame *pMain = g_Platform.GetMainPtr();

			if (!pMain)
			{
				err = "main window is not available";
				return;
			}

			std::error_code ec;

			if (!fs::exists(path, ec))
			{
				err = "file not found";
				return;
			}

			// Validate the header silently, then close it before the real load.
			DWORD type = 0;
			DWORD ver  = 0;
			{
				CMSFManager msf;

				if (!msf.OpenFile(path, true /*load*/, true /*silent*/))
				{
					err = "cannot open state file";
					return;
				}

				type = msf.GetType();
				ver  = msf.GetVersion();
			}

			if (type != MSF_STATE_ID)
			{
				err = "not a BK state image (MSF) file";
				return;
			}

			if (ver < MSF_VERSION_MINIMAL)
			{
				err = "state file version too old (unsupported)";
				return;
			}

			if (ver > MSF_VERSION_CURRENT)
			{
				err = "state file version too new (unsupported)";
				return;
			}

			pMain->LoadMemoryState(path);   // stop, load, reattach, reinit, run

			result["loaded"]  = true;
			result["path"]    = pstr;
			result["version"] = static_cast<int>(ver);
			result["running"] = g_Platform.IsCPURun();
			result["machine"] =
			{
				{ "config_id",   ToUtf8(g_ConfigBKModelParameters.at(g_Config.m_BKConfigModelNumber).strBKModelConfigName) },
				{ "config_name", ToUtf8(g_Config.GetBKConfName()) },
			};
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- disks -------------------------------------------------------------
	// Maps a "drive" argument (letter A-D) to an FDD_DRIVE.
	FDD_DRIVE::FDD_DRIVE ParseDrive(const json &args)
	{
		if (!args.contains("drive") || !args["drive"].is_string())
		{
			throw std::runtime_error("drive (letter A-D) is required");
		}

		const std::string s = args["drive"].get<std::string>();
		const size_t      b = s.find_first_not_of(" \t");

		if (b != std::string::npos)
		{
			char c = s[b];

			if (c >= 'a' && c <= 'z')
			{
				c = static_cast<char>(c - 'a' + 'A');
			}

			switch (c)
			{
				case 'A': return FDD_DRIVE::A;
				case 'B': return FDD_DRIVE::B;
				case 'C': return FDD_DRIVE::C;
				case 'D': return FDD_DRIVE::D;
			}
		}

		throw std::runtime_error("invalid drive (expected A, B, C or D)");
	}

	const char *DriveName(FDD_DRIVE::FDD_DRIVE d)
	{
		switch (d)
		{
			case FDD_DRIVE::A: return "A";
			case FDD_DRIVE::B: return "B";
			case FDD_DRIVE::C: return "C";
			case FDD_DRIVE::D: return "D";
			default:           return "?";
		}
	}

	// --- disk_attach -------------------------------------------------------
	// Mount a floppy image into drive A-D (live, like the UI - no CPU stop).
	json Tool_DiskAttach(const json &args)
	{
		const FDD_DRIVE::FDD_DRIVE drive = ParseDrive(args);

		if (!args.contains("path") || !args["path"].is_string())
		{
			throw std::runtime_error("path (string) is required");
		}

		const std::string pstr = args["path"].get<std::string>();
		const fs::path     path = Utf8ToPath(pstr);

		// CFloppyDrive::Attach fails silently on a missing file, so check here.
		std::error_code ec;

		if (!fs::exists(path, ec) || fs::is_directory(path, ec))
		{
			throw std::runtime_error("image file not found");
		}

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CFDDStd326 *pFDD = g_Platform.GetFDDPtr();

			if (!pFDD)
			{
				err = "no floppy controller in this configuration";
				return;
			}

			g_Config.SetDriveImgName(drive, path);
			pFDD->AttachImage(drive, path);

			// AttachImage returns true whenever the drive exists, even if the file
			// could not be opened, so confirm with IsAttached and roll back if not.
			if (!pFDD->IsAttached(drive))
			{
				g_Config.SetDriveImgName(drive, { g_strEmptyUnit.c_str() });
				err = "failed to attach image (could not open it as a disk)";
				return;
			}

			if (CMainFrame *pMain = g_Platform.GetMainPtr())
			{
				pMain->UpdateToolbarDriveIcons();
			}

			result["attached"]  = true;
			result["drive"]     = DriveName(drive);
			result["path"]      = pstr;
			result["read_only"] = pFDD->IsReadOnly(drive);
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	// --- disk_detach -------------------------------------------------------
	// Eject whatever image is in drive A-D.
	json Tool_DiskDetach(const json &args)
	{
		const FDD_DRIVE::FDD_DRIVE drive = ParseDrive(args);

		json        result;
		std::string err;

		const bool ok = mcp::Run([&]
		{
			CFDDStd326 *pFDD = g_Platform.GetFDDPtr();

			if (!pFDD)
			{
				err = "no floppy controller in this configuration";
				return;
			}

			const bool was = pFDD->IsAttached(drive);
			pFDD->DetachImage(drive);
			g_Config.SetDriveImgName(drive, { g_strEmptyUnit.c_str() });

			if (CMainFrame *pMain = g_Platform.GetMainPtr())
			{
				pMain->UpdateToolbarDriveIcons();
			}

			result["detached"]     = true;
			result["drive"]        = DriveName(drive);
			result["was_attached"] = was;
		});

		if (!ok)
		{
			throw std::runtime_error("emulator UI thread is not ready");
		}

		if (!err.empty())
		{
			throw std::runtime_error(err);
		}

		return result;
	}

	std::vector<McpTool> BuildToolRegistry()
	{
		std::vector<McpTool> tools;

		tools.push_back(
		{
			"cpu_state",
			"Read the CPU state: general registers r0-r5, SP, PC, PSW with "
			"decoded flags, run/break status and the last executed instruction. "
			"Numeric values are given both in octal (BK convention) and decimal.",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_CpuState
		});

		tools.push_back(
		{
			"sys_info",
			"Report environment info: the MCP server (name/version/protocol/port), "
			"the emulated machine and configuration (model id and human-readable "
			"name, BK10/11/11M family), CPU clock (current/base/min/max in Hz) and "
			"run state, and which peripherals are attached (FDD, HDD, AltPro, "
			"OPTOK, IRPS, AZBK).",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_SysInfo
		});

		tools.push_back(
		{
			"disasm",
			"Disassemble a run of instructions. 'addr' is the start address "
			"(octal string by default, or 0x-prefixed hex, or a decimal integer); "
			"if omitted, disassembly starts at the current PC. 'count' is the "
			"number of instructions (default 16, max 64). Returns, per instruction, "
			"its address, raw words and text, all in octal, plus the address that "
			"follows the last instruction.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "addr",  { { "type", json::array({ "string", "integer" }) }, { "description", "start address; default = current PC" } } },
						{ "count", { { "type", "integer" }, { "minimum", 1 }, { "maximum", DISASM_MAX_COUNT }, { "description", "instruction count (default 16)" } } },
					}
				},
			},
			&Tool_Disasm
		});

		tools.push_back(
		{
			"mem_read",
			"Read a range of memory without bus side effects. 'addr' is the start "
			"address (octal string by default, or 0x-prefixed hex, or a decimal "
			"integer) and is required. 'len' is the number of bytes (decimal, "
			"default 64, max 4096). Returns the start address and the address just "
			"past the range (octal), the byte count, a compact lowercase 'hex' "
			"string (two digits per byte, no separators), and an 'ascii' rendering "
			"(non-printable bytes shown as '.').",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "addr", { { "type", json::array({ "string", "integer" }) }, { "description", "start address (required)" } } },
						{ "len",  { { "type", "integer" }, { "minimum", 1 }, { "maximum", MEM_READ_MAX_LEN }, { "description", "byte count (default 64)" } } },
					}
				},
				{ "required", json::array({ "addr" }) },
			},
			&Tool_MemRead
		});

		tools.push_back(
		{
			"screen",
			"Capture the current emulator display as a PNG image (returned as an "
			"image the model can see). 'size' is 'native' (raw texture, 512x256 "
			"for BK / 1024x768 for AZBK; the default) or 'view' (stretched to the "
			"current aspect-corrected viewport). The screen is captured exactly as "
			"shown now (current colour/BW/adaptive mode and palette), which are "
			"reported in the accompanying metadata.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "size", { { "type", "string" }, { "enum", json::array({ "native", "view" }) }, { "description", "native (default) or view" } } },
					}
				},
			},
			&Tool_Screen
		});

		tools.push_back(
		{
			"emu_run",
			"Resume free-running execution: leave the debug break if the CPU is "
			"paused, or start it if it was stopped. Returns the resulting run/break "
			"state, whether anything changed, and the current PC.",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_EmuRun
		});

		tools.push_back(
		{
			"debug_break",
			"Pause the CPU into the debugger at the current PC. Idempotent: if it "
			"is already paused, nothing changes. Returns the resulting run/break "
			"state, whether anything changed, and the PC where it stopped.",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_DebugBreak
		});

		tools.push_back(
		{
			"debug_step",
			"Single-step the paused CPU. 'mode' is one of: 'into' (one "
			"instruction, entering calls/traps; the default), 'over' (one "
			"instruction, running subroutine calls to completion), 'out' (run "
			"until the current subroutine returns), or 'skip' (advance PC past the "
			"current instruction without executing it). Requires the CPU to be "
			"paused (see debug_break). Returns the resulting run/break state, the "
			"new PC, and the instruction now at the PC; 'completed' is false if an "
			"into/over/out step timed out while still running.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "mode", { { "type", "string" }, { "enum", json::array({ "into", "over", "out", "skip" }) }, { "description", "step mode (default into)" } } },
					}
				},
			},
			&Tool_DebugStep
		});

		tools.push_back(
		{
			"emu_reset",
			"Reset the board (equivalent to the reset button): a cold reset of the "
			"CPU followed by a restart. Optional hardware modifiers: 'su' (СУ/reset "
			"- restart at 100000 on BK11; no effect on BK10) and 'long_reset' (long "
			"reset, relevant to the A16M controller), both default false. Returns "
			"the post-reset run/break state and PC.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "su",         { { "type", "boolean" }, { "description", "СУ/reset: restart at 100000 (BK11)" } } },
						{ "long_reset", { { "type", "boolean" }, { "description", "long reset (A16M controller)" } } },
					}
				},
			},
			&Tool_EmuReset
		});

		tools.push_back(
		{
			"bp_list",
			"List all breakpoints. Each entry has its index, address (octal), type "
			"('exec' or 'access'), enabled and breaked (currently triggered) flags, "
			"and for access breakpoints the read/write/byte access flags.",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_BpList
		});

		tools.push_back(
		{
			"bp_set",
			"Add a breakpoint. 'addr' (octal string by default, or 0x-hex, or "
			"decimal) is required. 'type' is 'exec' (break when the PC reaches the "
			"address; the default) or 'access' (break on memory access). For "
			"'access', 'access' is a string of letters r/w/b: r=read, w=write, "
			"b=also catch byte-sized accesses (without b only word accesses "
			"trigger); default 'rwb'. Adding a duplicate of the same type+address "
			"is a no-op ('added' false).",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "addr",   { { "type", json::array({ "string", "integer" }) }, { "description", "breakpoint address (required)" } } },
						{ "type",   { { "type", "string" }, { "enum", json::array({ "exec", "access" }) }, { "description", "exec (default) or access" } } },
						{ "access", { { "type", "string" }, { "description", "for access type: letters r/w/b (default rwb)" } } },
					}
				},
				{ "required", json::array({ "addr" }) },
			},
			&Tool_BpSet
		});

		tools.push_back(
		{
			"bp_remove",
			"Remove every breakpoint (exec and access) at an address. Returns the "
			"number removed and the remaining breakpoint count.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "addr", { { "type", json::array({ "string", "integer" }) }, { "description", "address to remove (required)" } } },
					}
				},
				{ "required", json::array({ "addr" }) },
			},
			&Tool_BpRemove
		});

		tools.push_back(
		{
			"bp_clear",
			"Remove all breakpoints. Returns how many were cleared.",
			json{ { "type", "object" }, { "properties", json::object() } },
			&Tool_BpClear
		});

		tools.push_back(
		{
			"state_save",
			"Save a full emulator state image (MSF) to a file. 'path' (required) is "
			"the destination file path; it is created or overwritten. The CPU is "
			"briefly paused for a consistent snapshot, then resumed. Returns the "
			"path and the file size in bytes.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "path", { { "type", "string" }, { "description", "destination .msf file path (required)" } } },
					}
				},
				{ "required", json::array({ "path" }) },
			},
			&Tool_StateSave
		});

		tools.push_back(
		{
			"state_load",
			"Load a full emulator state image (MSF) from a file. 'path' (required) "
			"is an existing state file. The file is validated first; loading "
			"reconfigures the platform to the saved machine and resumes it. Returns "
			"the loaded machine's config id and name, the file version, and run "
			"state.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "path", { { "type", "string" }, { "description", "existing .msf state file path (required)" } } },
					}
				},
				{ "required", json::array({ "path" }) },
			},
			&Tool_StateLoad
		});

		tools.push_back(
		{
			"disk_attach",
			"Mount a floppy image into a drive. 'drive' is a letter A-D (required) "
			"and 'path' is the image file path (required). The mount is live (no CPU "
			"stop), as in the GUI. Returns the drive, path and whether it mounted "
			"read-only. Errors if the file is missing, the image cannot be opened, "
			"or the configuration has no floppy controller.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "drive", { { "type", "string" }, { "description", "drive letter A, B, C or D (required)" } } },
						{ "path",  { { "type", "string" }, { "description", "floppy image file path (required)" } } },
					}
				},
				{ "required", json::array({ "drive", "path" }) },
			},
			&Tool_DiskAttach
		});

		tools.push_back(
		{
			"disk_detach",
			"Eject the floppy image from a drive. 'drive' is a letter A-D "
			"(required). Returns the drive and whether an image had been attached.",
			json
			{
				{ "type", "object" },
				{
					"properties",
					{
						{ "drive", { { "type", "string" }, { "description", "drive letter A, B, C or D (required)" } } },
					}
				},
				{ "required", json::array({ "drive" }) },
			},
			&Tool_DiskDetach
		});

		return tools;
	}
}

//////////////////////////////////////////////////////////////////////////////
// Server implementation
//////////////////////////////////////////////////////////////////////////////

struct CMcpServer::Impl
{
	httplib::Server      svr;
	std::thread          listenThread;
	std::vector<McpTool> tools;
	std::string          sessionId;
	unsigned short       port{ 0 };
	std::atomic<bool>    listening{ false };

	const McpTool *FindTool(const std::string &name) const
	{
		for (const auto &t : tools)
		{
			if (t.name == name)
			{
				return &t;
			}
		}

		return nullptr;
	}

	// Builds a JSON-RPC error object response.
	static json RpcError(const json &id, int code, const std::string &message)
	{
		return json
		{
			{ "jsonrpc", "2.0" },
			{ "id", id },
			{ "error", { { "code", code }, { "message", message } } },
		};
	}

	static json RpcResult(const json &id, json result)
	{
		return json
		{
			{ "jsonrpc", "2.0" },
			{ "id", id },
			{ "result", std::move(result) },
		};
	}

	// Handles one JSON-RPC message. Returns the response object, or a null json
	// for notifications (no response expected).
	json HandleRpc(const json &req)
	{
		if (!req.is_object() || req.value("jsonrpc", "") != "2.0" || !req.contains("method"))
		{
			return RpcError(req.value("id", json(nullptr)), -32600, "Invalid Request");
		}

		const std::string method = req.value("method", "");
		const json        id     = req.contains("id") ? req["id"] : json(nullptr);
		const bool        isNotification = !req.contains("id");
		const json        params = req.value("params", json::object());

		// Notifications (e.g. notifications/initialized) get no response.
		if (isNotification)
		{
			return json(nullptr);
		}

		if (method == "initialize")
		{
			const std::string pv = params.value("protocolVersion", std::string(MCP_PROTOCOL_VERSION));
			return RpcResult(id, json
			{
				{ "protocolVersion", pv },
				{ "capabilities", { { "tools", { { "listChanged", false } } } } },
				{ "serverInfo", { { "name", MCP_SERVER_NAME }, { "version", MCP_SERVER_VERSION } } },
			});
		}

		if (method == "ping")
		{
			return RpcResult(id, json::object());
		}

		if (method == "tools/list")
		{
			json arr = json::array();

			for (const auto &t : tools)
			{
				arr.push_back(json
				{
					{ "name", t.name },
					{ "description", t.description },
					{ "inputSchema", t.inputSchema },
				});
			}

			return RpcResult(id, json{ { "tools", arr } });
		}

		if (method == "tools/call")
		{
			const std::string name = params.value("name", "");
			const json        args = params.value("arguments", json::object());
			const McpTool    *tool = FindTool(name);

			if (!tool)
			{
				return RpcError(id, -32602, "Unknown tool: " + name);
			}

			// Tool execution errors are returned as a result with isError=true so
			// the model can see and react to them, per the MCP convention.
			try
			{
				const json data = tool->handler(args);

				// A tool may return rich content (e.g. an image) by providing a
				// "_content" array; otherwise its JSON is wrapped as one text block.
				json content;

				if (data.is_object() && data.contains("_content") && data["_content"].is_array())
				{
					content = data["_content"];
				}
				else
				{
					content = json::array({ json{ { "type", "text" }, { "text", data.dump(2) } } });
				}

				return RpcResult(id, json
				{
					{ "content", content },
					{ "isError", false },
				});
			}
			catch (const std::exception &e)
			{
				return RpcResult(id, json
				{
					{ "content", json::array({ json{ { "type", "text" }, { "text", std::string("Error: ") + e.what() } } }) },
					{ "isError", true },
				});
			}
		}

		return RpcError(id, -32601, "Method not found: " + method);
	}
};

CMcpServer g_McpServer;

CMcpServer::CMcpServer() : m_pImpl(std::make_unique<Impl>())
{
}

CMcpServer::~CMcpServer()
{
	Stop();
}

bool CMcpServer::IsRunning() const
{
	return m_pImpl->listening.load();
}

unsigned short CMcpServer::GetPort() const
{
	return m_pImpl->port;
}

bool CMcpServer::Start(unsigned short port)
{
	Impl &impl = *m_pImpl;

	if (impl.listening.load() || impl.listenThread.joinable())
	{
		return false;
	}

	impl.tools = BuildToolRegistry();
	impl.port = port;
	impl.sessionId = "bkemu-" + std::to_string(::GetTickCount64());

	// Single MCP endpoint. POST carries JSON-RPC; GET would open a server->client
	// SSE stream, which we don't offer (request/response only) -> 405.
	impl.svr.Post("/mcp", [&impl](const httplib::Request &req, httplib::Response &res)
	{
		res.set_header("Mcp-Session-Id", impl.sessionId);

		json body;

		try
		{
			body = json::parse(req.body);
		}
		catch (const std::exception &)
		{
			res.status = 400;
			res.set_content(Impl::RpcError(json(nullptr), -32700, "Parse error").dump(),
			                "application/json");
			return;
		}

		if (body.is_array())
		{
			json out = json::array();

			for (const auto &msg : body)
			{
				json r = impl.HandleRpc(msg);

				if (!r.is_null())
				{
					out.push_back(std::move(r));
				}
			}

			if (out.empty())
			{
				res.status = 202;   // only notifications -> no body
				return;
			}

			res.set_content(out.dump(), "application/json");
		}
		else
		{
			json r = impl.HandleRpc(body);

			if (r.is_null())
			{
				res.status = 202;   // notification -> no body
				return;
			}

			res.set_content(r.dump(), "application/json");
		}
	});

	impl.svr.Get("/mcp", [](const httplib::Request &, httplib::Response &res)
	{
		res.status = 405;   // we don't provide a server-initiated SSE stream
	});

	impl.listenThread = std::thread([&impl, port]
	{
		impl.listening.store(true);
		impl.svr.listen("127.0.0.1", port);   // blocks until svr.stop()
		impl.listening.store(false);
	});

	// Give the listener a moment to bind, then report whether it is up.
	impl.svr.wait_until_ready();

	if (!impl.svr.is_running())
	{
		Stop();
		return false;
	}

	return true;
}

void CMcpServer::Stop()
{
	Impl &impl = *m_pImpl;

	if (impl.svr.is_running())
	{
		impl.svr.stop();
	}

	if (impl.listenThread.joinable())
	{
		impl.listenThread.join();
	}

	impl.listening.store(false);
}
